Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MDEV-19191 Partial support of foreign keys in partitioned tables #3641

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

midenok
Copy link
Contributor

@midenok midenok commented Nov 19, 2024

The patch adds the ability to run foreign keys in partitioned tables with
limitations.

Example:

  create or replace table t1 (id int primary key) engine innodb;
  create or replace table t2 (fk int references t1(id)) engine innodb
  partition by hash (fk) partitions 2;

Limitations:

  1. Foreign keys cannot refer partitioned table even SYSTEM_TIME-partitioned
  still. create_foreign_keys() at InnoDB layer receives Foreign_key about
  referenced table T, but no such table exists in InnoDB layer, only partition
  tables T#P#p0, T#P#p1, ..., T#P#pn. Finding out it is SYSTEM_TIME partitioning
  and current partition at that point is impossible without modification of
  SYS_TABLES or opening TABLE object of referenced table. Both should be avoided
  as this is superseded by MDEV-12483.

  2. CASCADE and SET NULL actions are disabled in partitioned foreign table as
  these actions update row data and this is the subject of row placement into
  another partition but it cannot be done in InnoDB layer. DELETE CASCADE for
  SYSTEM_TIME partitioning requires the row to be moved from current to history
  partition.

The task is basically divided into 3 parts:

  1. Remove prohibiting code, allow FKs to be created for partitions;
  2. Allow partitioned FKs at SQL layer for such functions as SHOW CREATE;
  3. Implement correct handling of FKs when partitioning configuration changes.

1. Remove prohibiting code, allow FKs to be created for partitions

  In SYS_FOREIGN table foreign key records are unique by ID which was taken from
  constraint name or automatically generated. Normally foreign ID and constraint
  name are identical, but that's not the case for partitioned table as InnoDB
  holds foreign keys per dict_table_t object and each partition is a different
  table for InnoDB. So for each foreign key in SQL layer there is a set of foreign
  keys in InnoDB layer per each partition (except SYSTEM_TIME partitioning where
  we keep foreign keys only for current partition). To constitute unique foreign
  ID at InnoDB layer we concatenate constraint name with partition suffix, the one
  what is added to partition table name beginning with #P# and optionally
  containing #SP# for subpartitions. Constraint name and partitioning suffix are
  separated by \xFF character which is the safe character code non-clashing with
  identifier character set.

  When we return back foreign ID to SQL layer this partitioning suffix is stripped
  off the constraint name and SQL output receives the name similar to
  non-partitioned table.

  User may see a bit more truthful version of foreign ID in
  INFORMATION_SCHEMA.INNODB_SYS_FOREIGN with \xFF replaced by ':' and #P# or
  everything starting from #P# and ending by #SP# chopped out. So he may see
  corresponding partition name or subpartition name in ID of INNODB_SYS_FOREIGN.

2. Allow partitioned FKs at SQL layer for such functions as SHOW CREATE

  Through standard handler interface get_foreign_key_list() foreign keys are
  returned to SQL layer. For SYSTEM_TIME partitioning from current partition, for
  any other partitioning from first read-marked partition.

3. Implement correct handling of FKs when partitioning configuration changes

  ALTER operations such as ADD PARTITION, DROP PARTITION, REMOVE PARTITIONING,
  etc. are reflected into correct configuration of foreign keys in InnoDB.
  Handling of foreign key ID for temporary tables in ALTER was done based on
  MDEV-28933.

When there are GCC-incompatible compiler flags dtrace fails like this:

gcc: error: unrecognized command-line option ‘-fno-limit-debug-info’
gcc: error: unrecognized command-line option ‘-mbranches-within-32B-boundaries’
"gcc .dtrace-temp.3fd6bacf.c" failed
Usage /usr/bin/dtrace [--help] [-h | -G] [-C [-I<Path>]] -s File.d [-o <File>]
Return true for temporary partitions.
  Based on Marko Makela's patch 4a591d4:

  dict_get_referenced_table(): Let the caller convert names when needed.

  row_rename_table_for_mysql(): Specify the treatment of FOREIGN KEY
  constraints in a 4-valued enum parameter. In cases where FOREIGN KEY
  constraints cannot exist (partitioned tables, or internal tables of
  FULLTEXT INDEX), we can use the mode RENAME_IGNORE_FK.
  The mod RENAME_REBUILD is for any DDL operation that rebuilds the
  table inside InnoDB, such as TRUNCATE and native ALTER TABLE
  (or OPTIMIZE TABLE). The mode RENAME_ALTER_COPY is used solely
  during non-native ALTER TABLE in ha_innobase::rename_table().
  Normal ha_innobase::rename_table() will use the mode RENAME_FK.

  CREATE OR REPLACE will rename the old table (if one exists) along
  with its FOREIGN KEY constraints into a temporary name. The replacement
  table will be initially created with another temporary name.
  Unlike in ALTER TABLE, all FOREIGN KEY constraints must be renamed
  and not inherited as part of these operations, using the mode RENAME_FK.

  dict_get_referenced_table(): Let the callers convert names when needed.

  create_table_info_t::create_foreign_keys(): CREATE OR REPLACE creates
  the replacement table with a temporary name table, so for
  self-references foreign->referenced_table will be a table with
  temporary name and charset conversion must be skipped for it.
All InnoDB internal SQL functions should be moved to sql_funcs.h
Use temporary constraint names for temporary tables. The constraints
are not added to cache (skipped in dict_table_rename_in_cache()).

The scheme for temporary constraint names is as follows:

    for old table: db_name/\xFFconstraint_name
    for new table: db_name/\xFF\xFFconstraint_name

normalize_table_name_c_low(): wrong comparison "less than FN_REFLEN -
1". Somewhere array of FN_REFLEN includes the trailing 0, somewhere
array of FN_REFLEN + 1 includes trailing 0, but nowhere array of
FN_REFLEN - 1 must include trailing 0.

Change from original MDEV-28933 fix:

As temporary FK is now required for any ALTER operation (especially
partitioning operations) row_rename_table_for_mysql() does FK rename
on both RENAME_FK and RENAME_ALTER_COPY (see do_rename_fk).
Unused code, assertions, comments, etc
The patch adds the ability to run foreign keys in partitioned tables with
limitations.

Example:

  create or replace table t1 (id int primary key) engine innodb;
  create or replace table t2 (fk int references t1(id)) engine innodb
  partition by hash (fk) partitions 2;

Limitations:

  1. Foreign keys cannot refer partitioned table even SYSTEM_TIME-partitioned
  still. create_foreign_keys() at InnoDB layer receives Foreign_key about
  referenced table T, but no such table exists in InnoDB layer, only partition
  tables T#P#p0, T#P#p1, ..., T#P#pn. Finding out it is SYSTEM_TIME partitioning
  and current partition at that point is impossible without modification of
  SYS_TABLES or opening TABLE object of referenced table. Both should be avoided
  as this is superseded by MDEV-12483.

  2. CASCADE and SET NULL actions are disabled in partitioned foreign table as
  these actions update row data and this is the subject of row placement into
  another partition but it cannot be done in InnoDB layer. DELETE CASCADE for
  SYSTEM_TIME partitioning requires the row to be moved from current to history
  partition.

The task is basically divided into 3 parts:

  1. Remove prohibiting code, allow FKs to be created for partitions;
  2. Allow partitioned FKs at SQL layer for such functions as SHOW CREATE;
  3. Implement correct handling of FKs when partitioning configuration changes.

1. Remove prohibiting code, allow FKs to be created for partitions

  In SYS_FOREIGN table foreign key records are unique by ID which was taken from
  constraint name or automatically generated. Normally foreign ID and constraint
  name are identical, but that's not the case for partitioned table as InnoDB
  holds foreign keys per dict_table_t object and each partition is a different
  table for InnoDB. So for each foreign key in SQL layer there is a set of foreign
  keys in InnoDB layer per each partition (except SYSTEM_TIME partitioning where
  we keep foreign keys only for current partition). To constitute unique foreign
  ID at InnoDB layer we concatenate constraint name with partition suffix, the one
  what is added to partition table name beginning with #P# and optionally
  containing #SP# for subpartitions. Constraint name and partitioning suffix are
  separated by \xFF character which is the safe character code non-clashing with
  identifier character set.

  When we return back foreign ID to SQL layer this partitioning suffix is stripped
  off the constraint name and SQL output receives the name similar to
  non-partitioned table.

  User may see a bit more truthful version of foreign ID in
  INFORMATION_SCHEMA.INNODB_SYS_FOREIGN with \xFF replaced by ':' and #P# or
  everything starting from #P# and ending by #SP# chopped out. So he may see
  corresponding partition name or subpartition name in ID of INNODB_SYS_FOREIGN.

2. Allow partitioned FKs at SQL layer for such functions as SHOW CREATE

  Through standard handler interface get_foreign_key_list() foreign keys are
  returned to SQL layer. For SYSTEM_TIME partitioning from current partition, for
  any other partitioning from first read-marked partition.

3. Implement correct handling of FKs when partitioning configuration changes

  ALTER operations such as ADD PARTITION, DROP PARTITION, REMOVE PARTITIONING,
  etc. are reflected into correct configuration of foreign keys in InnoDB.
  Handling of foreign key ID for temporary tables in ALTER was done based on
  MDEV-28933.
This commit is not intended to be pushed into main branch. Its mission
is to simplify the review process. The comments here are in different
style: // and without indentation. The reviewer may ask for some of
them to be included into the task code. In that case they will be
reformatted and moved to the task commit.
Related to FIXME here. Now check_bulk_buffer() returns false.

3389                    if (auto t= trx->check_bulk_buffer(index->table)) {
3390                            /* MDEV-25036 FIXME:
3391                            row_ins_check_foreign_constraint() check
3392                            should be done before buffering the insert
3393                            operation. */
3394                            ut_ad(index->table->skip_alter_undo
3395                                  || !trx->check_foreigns);
3396                            return t->bulk_insert_buffered(*entry, *index, trx);
3397                    }
@midenok midenok requested a review from dr-m November 19, 2024 16:46
@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Contributor

@dr-m dr-m left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are some initial review comments.

Something needs to be done about MDEV-33489, because the test atomic.alter_table is timing out on several builders for this pull request.

What does the change 07e23fc have to do with this? It should be submitted as a separate pull request, with as separate MDEV number.

There are InnoDB changes in several commits, with messages that fail to specify why something was changed, or even to describe what was changed. An example is dbdade6. Each change needs to be covered with test cases.

@@ -604,7 +604,6 @@ class ha_mroonga: public handler
int index_last(uchar *buf) mrn_override;
#endif
void change_table_ptr(TABLE *table_arg, TABLE_SHARE *share_arg) mrn_override;
bool is_fk_defined_on_table_or_index(uint index) mrn_override;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commit message 7d6cb82 fails to explain why this was removed. Were the mroonga tests run at all after changing this? Can you point us to a build linked from [the grid view of this pull request|https://buildbot.mariadb.org/#/grid?branch=refs%2Fpull%2F3641%2Fmerge] that covers both the Mroonga storage and wrapper tests?

Comment on lines -295 to +299
--error ER_FEATURE_NOT_SUPPORTED_WITH_PARTITIONING
CREATE TABLE t1 (a INT, FOREIGN KEY (a) REFERENCES t0 (a))
ENGINE=MyISAM
PARTITION BY HASH (a);
show create table t1;
drop table t1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not sufficient to demonstrate that the table can be created. Some functional testing is needed as well, to demonstrate that the foreign keys actually work.

Given that this test uses ENGINE=MyISAM and FOREIGN KEY constraints are currently implemented only in ENGINE=InnoDB, this test seem to demonstrate that things are changing for the worse. We’d allow a DDL operation that was previously rejected because it was known not to work. I don’t think that this change is acceptable.

Comment on lines -240 to 247
--error ER_FEATURE_NOT_SUPPORTED_WITH_PARTITIONING
ALTER TABLE t1 ADD CONSTRAINT test_ibfk_1 FOREIGN KEY (parent_id) REFERENCES t2 (id);

ALTER TABLE t1 PARTITION BY HASH (id) PARTITIONS 2;

--error ER_FEATURE_NOT_SUPPORTED_WITH_PARTITIONING
--error ER_CANT_CREATE_TABLE
ALTER TABLE t1 ADD CONSTRAINT test_ibfk_1 FOREIGN KEY (parent_id) REFERENCES t2 (id);

DROP TABLE t1, t2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some functional testing with DML statements is needed here to demonstrate that the FOREIGN KEY constraint works after each of these ALTER TABLE operation, including the rejected one.

Please also add a comment here that there are further tests about this in parts.foreign. Initially, I was going to ask for this to be extended to cover ON (UPDATE|DELETE) (CASCADE|SET NULL), but those are covered in that test.

Comment on lines 247 to +250
ALTER TABLE t1 ADD CONSTRAINT test_ibfk_1 FOREIGN KEY (parent_id) REFERENCES t2 (id);
ERROR HY000: Partitioned tables do not support FOREIGN KEY
ALTER TABLE t1 PARTITION BY HASH (id) PARTITIONS 2;
ALTER TABLE t1 ADD CONSTRAINT test_ibfk_1 FOREIGN KEY (parent_id) REFERENCES t2 (id);
ERROR HY000: Partitioned tables do not support FOREIGN KEY
ERROR HY000: Can't create table `test`.`t1` (errno: 150 "Foreign key constraint is incorrectly formed")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message looks incorrect here. Shouldn’t this fail because a duplicate constraint name test_ibfk_1? Would it work with a different constraint name?

+++ foreign_null,COPY.result
+++ foreign_null,COPY.reject
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this line changed?

Comment on lines 511 to +512
alter table t3 add constraint dc foreign key (a) references t1(a);
ERROR HY000: Can't create table `test`.`t3` (errno: 121 "Duplicate key on write or update")
ERROR HY000: Duplicate constraint name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message fails to include the duplicate constraint name dc.

select * from information_schema.innodb_sys_foreign_cols;
show create table u;
use test;
eval drop database `$d`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing an --echo # End of 11.x tests marker.

@janlindstrom
Copy link
Contributor

@midenok Unfortunately, I do not see any replication tests or Galera tests that demonstrates this new feature does work. Functional testing seems to focus can you create foreign key on portioned table but fails to test does actual foreign key work and RESTRICT action work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants