From 9b431d714fdf030e372ed6eeaec0619dcffdde6a Mon Sep 17 00:00:00 2001 From: Yuchen Pei Date: Tue, 4 Jul 2023 17:56:27 +1000 Subject: [PATCH] MDEV-26137 Improve import tablespace workflow. Allow ALTER TABLE ... IMPORT TABLESPACE without creating the table followed by discarding the tablespace. That is, assuming we want to import table t1 to t2, instead of CREATE TABLE t2 LIKE t1; ALTER TABLE t2 DISCARD TABLESPACE; FLUSH TABLES t1 FOR EXPORT; --copy_file $MYSQLD_DATADIR/test/t1.cfg $MYSQLD_DATADIR/test/t2.cfg --copy_file $MYSQLD_DATADIR/test/t1.ibd $MYSQLD_DATADIR/test/t2.ibd UNLOCK TABLES; ALTER TABLE t2 IMPORT TABLESPACE; We can simply do FLUSH TABLES t1 FOR EXPORT; --copy_file $MYSQLD_DATADIR/test/t1.cfg $MYSQLD_DATADIR/test/t2.cfg --copy_file $MYSQLD_DATADIR/test/t1.frm $MYSQLD_DATADIR/test/t2.frm --copy_file $MYSQLD_DATADIR/test/t1.ibd $MYSQLD_DATADIR/test/t2.ibd UNLOCK TABLES; ALTER TABLE t2 IMPORT TABLESPACE; We achieve this by creating a "stub" table in the second scenario while opening the table, where t2 does not exist but needs to import from t1. The "stub" table is similar to a table that is created but then instructed to discard its tablespace. We include tests with various row formats, encryption, with indexes and auto-increment. --- .../suite/encryption/r/innodb_import.result | 19 ++ .../suite/encryption/t/innodb_import.test | 19 ++ .../suite/innodb/include/import_begin.inc | 32 +++ .../suite/innodb/include/import_end.inc | 8 + .../include/innodb_row_format_2.combinations | 5 + .../innodb/include/innodb_row_format_2.inc | 4 + mysql-test/suite/innodb/r/import.result | 18 ++ .../suite/innodb/r/import_no_cfg.result | 21 ++ .../suite/innodb/r/import_recovery.result | 80 ++++++ .../suite/innodb/r/import_run_once.result | 104 +++++++ mysql-test/suite/innodb/t/import.test | 3 + mysql-test/suite/innodb/t/import_no_cfg.test | 4 + .../suite/innodb/t/import_recovery.test | 163 +++++++++++ .../suite/innodb/t/import_run_once.test | 86 ++++++ sql/sql_yacc.yy | 1 + sql/structs.h | 4 +- storage/innobase/handler/ha_innodb.cc | 37 ++- storage/innobase/handler/ha_innodb.h | 5 + storage/innobase/handler/handler0alter.cc | 9 + storage/innobase/include/dict0mem.h | 8 +- storage/innobase/include/row0import.h | 10 + storage/innobase/row/row0import.cc | 258 ++++++++++++++---- storage/innobase/row/row0mysql.cc | 9 + 23 files changed, 851 insertions(+), 56 deletions(-) create mode 100644 mysql-test/suite/innodb/include/import_begin.inc create mode 100644 mysql-test/suite/innodb/include/import_end.inc create mode 100644 mysql-test/suite/innodb/include/innodb_row_format_2.combinations create mode 100644 mysql-test/suite/innodb/include/innodb_row_format_2.inc create mode 100644 mysql-test/suite/innodb/r/import.result create mode 100644 mysql-test/suite/innodb/r/import_no_cfg.result create mode 100644 mysql-test/suite/innodb/r/import_recovery.result create mode 100644 mysql-test/suite/innodb/r/import_run_once.result create mode 100644 mysql-test/suite/innodb/t/import.test create mode 100644 mysql-test/suite/innodb/t/import_no_cfg.test create mode 100644 mysql-test/suite/innodb/t/import_recovery.test create mode 100644 mysql-test/suite/innodb/t/import_run_once.test diff --git a/mysql-test/suite/encryption/r/innodb_import.result b/mysql-test/suite/encryption/r/innodb_import.result index 54b95ab26d4..b1895b65ef7 100644 --- a/mysql-test/suite/encryption/r/innodb_import.result +++ b/mysql-test/suite/encryption/r/innodb_import.result @@ -20,3 +20,22 @@ SELECT * FROM t2; f1 f2 1 InnoDB DROP TABLE t1, t2; +# +# MDEV-26137 ALTER TABLE IMPORT enhancement +# +# with encryption and page_compressed +CREATE TABLE t1 (a int, b varchar(50)) ENGINE=InnoDB ENCRYPTED=YES ENCRYPTION_KEY_ID=6 PAGE_COMPRESSED=1; +INSERT INTO t1 VALUES(42, "hello"); +FLUSH TABLES t1 FOR EXPORT; +UNLOCK TABLES; +ALTER TABLE t2 IMPORT TABLESPACE; +SHOW CREATE TABLE t2; +Table Create Table +t2 CREATE TABLE `t2` ( + `a` int(11) DEFAULT NULL, + `b` varchar(50) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci `ENCRYPTED`=YES `ENCRYPTION_KEY_ID`=6 `PAGE_COMPRESSED`=1 +SELECT * FROM t2; +a b +42 hello +DROP TABLE t1, t2; diff --git a/mysql-test/suite/encryption/t/innodb_import.test b/mysql-test/suite/encryption/t/innodb_import.test index 2e5470c5568..91bae752530 100644 --- a/mysql-test/suite/encryption/t/innodb_import.test +++ b/mysql-test/suite/encryption/t/innodb_import.test @@ -21,3 +21,22 @@ ALTER TABLE t2 DROP KEY idx; ALTER TABLE t2 IMPORT TABLESPACE; SELECT * FROM t2; DROP TABLE t1, t2; + +--echo # +--echo # MDEV-26137 ALTER TABLE IMPORT enhancement +--echo # + +--echo # with encryption and page_compressed +CREATE TABLE t1 (a int, b varchar(50)) ENGINE=InnoDB ENCRYPTED=YES ENCRYPTION_KEY_ID=6 PAGE_COMPRESSED=1; +INSERT INTO t1 VALUES(42, "hello"); +FLUSH TABLES t1 FOR EXPORT; +--copy_file $MYSQLD_DATADIR/test/t1.cfg $MYSQLD_DATADIR/test/t2.cfg +--copy_file $MYSQLD_DATADIR/test/t1.frm $MYSQLD_DATADIR/test/t2.frm +--copy_file $MYSQLD_DATADIR/test/t1.ibd $MYSQLD_DATADIR/test/t2.ibd +UNLOCK TABLES; +ALTER TABLE t2 IMPORT TABLESPACE; +SHOW CREATE TABLE t2; +SELECT * FROM t2; +DROP TABLE t1, t2; + +# Embedded server uses absolute path, causing result mismatch in warning messages when .cfg is not copied over. Given we have tested importing without copying cfg in other tests, we don't do it here. diff --git a/mysql-test/suite/innodb/include/import_begin.inc b/mysql-test/suite/innodb/include/import_begin.inc new file mode 100644 index 00000000000..5ee46b52430 --- /dev/null +++ b/mysql-test/suite/innodb/include/import_begin.inc @@ -0,0 +1,32 @@ +--source include/innodb_row_format.inc +--source include/innodb_row_format_2.inc +--source include/innodb_checksum_algorithm.inc + +--echo # +--echo # MDEV-26137 ALTER TABLE IMPORT enhancement +--echo # + +let $MYSQLD_DATADIR = `SELECT @@datadir`; + +--disable_query_log +let $ROW_FORMAT_OPTION=; +if($MTR_COMBINATION_R_REDUNDANT) { + let $ROW_FORMAT_OPTION= ROW_FORMAT=REDUNDANT; +} +if($MTR_COMBINATION_R_COMPACT) { + let $ROW_FORMAT_OPTION= ROW_FORMAT=COMPACT; +} +if($MTR_COMBINATION_R_DYNAMIC) { + let $ROW_FORMAT_OPTION= ROW_FORMAT=DYNAMIC; +} +if($MTR_COMBINATION_R_COMPRESSED) { + let $ROW_FORMAT_OPTION= ROW_FORMAT=COMPRESSED; +} +let $CREATE_OPTIONS_REGEX= /ENGINE=InnoDB.*$ROW_FORMAT_OPTION/CREATE_OPTIONS/; +--enable_query_log + +--replace_regex $CREATE_OPTIONS_REGEX +eval +CREATE TABLE t1(a INT PRIMARY KEY DEFAULT 42) ENGINE=InnoDB $ROW_FORMAT_OPTION; +INSERT INTO t1() VALUES(); +FLUSH TABLES t1 FOR EXPORT; diff --git a/mysql-test/suite/innodb/include/import_end.inc b/mysql-test/suite/innodb/include/import_end.inc new file mode 100644 index 00000000000..916306ea8d2 --- /dev/null +++ b/mysql-test/suite/innodb/include/import_end.inc @@ -0,0 +1,8 @@ +--copy_file $MYSQLD_DATADIR/test/t1.frm $MYSQLD_DATADIR/test/t2.frm +--copy_file $MYSQLD_DATADIR/test/t1.ibd $MYSQLD_DATADIR/test/t2.ibd +UNLOCK TABLES; +ALTER TABLE t2 IMPORT TABLESPACE; +--replace_regex $CREATE_OPTIONS_REGEX +SHOW CREATE TABLE t2; +SELECT * FROM t2; +DROP TABLE t1, t2; diff --git a/mysql-test/suite/innodb/include/innodb_row_format_2.combinations b/mysql-test/suite/innodb/include/innodb_row_format_2.combinations new file mode 100644 index 00000000000..14a0281df3f --- /dev/null +++ b/mysql-test/suite/innodb/include/innodb_row_format_2.combinations @@ -0,0 +1,5 @@ +[r_default] +[r_redundant] +[r_compact] +[r_dynamic] +[r_compressed] diff --git a/mysql-test/suite/innodb/include/innodb_row_format_2.inc b/mysql-test/suite/innodb/include/innodb_row_format_2.inc new file mode 100644 index 00000000000..2d42f74ac66 --- /dev/null +++ b/mysql-test/suite/innodb/include/innodb_row_format_2.inc @@ -0,0 +1,4 @@ +# The goal of including this file is to add various row_format options +# combinations (see include/innodb_row_format_2.combinations) + +--source include/have_innodb.inc diff --git a/mysql-test/suite/innodb/r/import.result b/mysql-test/suite/innodb/r/import.result new file mode 100644 index 00000000000..d1ed09456a5 --- /dev/null +++ b/mysql-test/suite/innodb/r/import.result @@ -0,0 +1,18 @@ +# +# MDEV-26137 ALTER TABLE IMPORT enhancement +# +CREATE TABLE t1(a INT PRIMARY KEY DEFAULT 42) CREATE_OPTIONS; +INSERT INTO t1() VALUES(); +FLUSH TABLES t1 FOR EXPORT; +UNLOCK TABLES; +ALTER TABLE t2 IMPORT TABLESPACE; +SHOW CREATE TABLE t2; +Table Create Table +t2 CREATE TABLE `t2` ( + `a` int(11) NOT NULL DEFAULT 42, + PRIMARY KEY (`a`) +) CREATE_OPTIONS +SELECT * FROM t2; +a +42 +DROP TABLE t1, t2; diff --git a/mysql-test/suite/innodb/r/import_no_cfg.result b/mysql-test/suite/innodb/r/import_no_cfg.result new file mode 100644 index 00000000000..13570db2e56 --- /dev/null +++ b/mysql-test/suite/innodb/r/import_no_cfg.result @@ -0,0 +1,21 @@ +# +# MDEV-26137 ALTER TABLE IMPORT enhancement +# +CREATE TABLE t1(a INT PRIMARY KEY DEFAULT 42) CREATE_OPTIONS; +INSERT INTO t1() VALUES(); +FLUSH TABLES t1 FOR EXPORT; +UNLOCK TABLES; +ALTER TABLE t2 IMPORT TABLESPACE; +Warnings: +Warning 1810 IO Read error: (2, No such file or directory) Error opening './test/t2.cfg', will attempt to import without schema verification +Warning 1810 IO Read error: (2, No such file or directory) Error opening './test/t2.cfg', will attempt to import without schema verification +SHOW CREATE TABLE t2; +Table Create Table +t2 CREATE TABLE `t2` ( + `a` int(11) NOT NULL DEFAULT 42, + PRIMARY KEY (`a`) +) CREATE_OPTIONS +SELECT * FROM t2; +a +42 +DROP TABLE t1, t2; diff --git a/mysql-test/suite/innodb/r/import_recovery.result b/mysql-test/suite/innodb/r/import_recovery.result new file mode 100644 index 00000000000..5b391ba65d3 --- /dev/null +++ b/mysql-test/suite/innodb/r/import_recovery.result @@ -0,0 +1,80 @@ +# +# MDEV-26137 ALTER TABLE IMPORT enhancement +# +call mtr.add_suppression('InnoDB: Tablespace for table `test`.`t1` is set as discarded.'); +call mtr.add_suppression('InnoDB: Tablespace for table `test`.`t2` is set as discarded.'); +call mtr.add_suppression('InnoDB: Tablespace for table `test`.`t3` is set as discarded.'); +call mtr.add_suppression('InnoDB: ./test/t3.ibd: Page 0 at offset 0 looks corrupted.'); +call mtr.add_suppression("mariadbd.*: Index for table 't3' is corrupt; try to repair it"); +call mtr.add_suppression("InnoDB: Expected tablespace id \\d+ but found \\d+ in the file ./test/t3.ibd"); +call mtr.add_suppression("InnoDB: Corrupted page \\[page id: space=.*, page number=0\\] of datafile './test/t3.ibd' could not be found in the doublewrite buffer."); +call mtr.add_suppression('InnoDB: Tablespace for table `test`.`t4` is set as discarded.'); +call mtr.add_suppression('InnoDB: ./test/t4.ibd: Page 0 at offset 0 looks corrupted.'); +call mtr.add_suppression("mariadbd.*: Index for table 't4' is corrupt; try to repair it"); +call mtr.add_suppression("InnoDB: Corrupted page \\[page id: space=.*, page number=0\\] of datafile './test/t4.ibd' could not be found in the doublewrite buffer."); +# Recovery from crashes +## t1: Creation of stub succeeds; server crashes; second import attempt succeeds +## t2: Creation of stub succeeds; server crashes; drop table +## t3: Creation of stub succeeds; server crashes; ibd corrupted; second import attempt fails; drop table +## t4: Did not copy .cfg; creation of stub succeeds; server crashes; ibd corrupted; second import attempt fails; drop table +CREATE TABLE t (a int) ENGINE=InnoDB; +INSERT INTO t VALUES(42); +FLUSH TABLES t FOR EXPORT; +UNLOCK TABLES; +SET GLOBAL innodb_max_dirty_pages_pct_lwm=0.0; +SET GLOBAL innodb_max_dirty_pages_pct=0.0; +connect hang1,localhost,root; +SET DEBUG_SYNC='ib_after_create_stub_for_import SIGNAL hung WAIT_FOR ever'; +ALTER TABLE t1 IMPORT TABLESPACE; +connection default; +SET DEBUG_SYNC='now WAIT_FOR hung'; +connect hang2,localhost,root; +SET DEBUG_SYNC='ib_after_create_stub_for_import SIGNAL hung WAIT_FOR ever'; +ALTER TABLE t2 IMPORT TABLESPACE; +connection default; +SET DEBUG_SYNC='now WAIT_FOR hung'; +connect hang3,localhost,root; +SET DEBUG_SYNC='ib_after_create_stub_for_import SIGNAL hung WAIT_FOR ever'; +ALTER TABLE t3 IMPORT TABLESPACE; +connection default; +SET DEBUG_SYNC='now WAIT_FOR hung'; +connect hang4,localhost,root; +SET DEBUG_SYNC='ib_after_create_stub_for_import SIGNAL hung WAIT_FOR ever'; +ALTER TABLE t4 IMPORT TABLESPACE; +connection default; +SET DEBUG_SYNC='now WAIT_FOR hung'; +# corrupting the 0th page +# Restart mysqld after the crash and reconnect. +# restart +ALTER TABLE t1 IMPORT TABLESPACE; +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci +SELECT * FROM t1; +a +42 +ALTER TABLE t3 IMPORT TABLESPACE; +ERROR HY000: Internal error: Error importing tablespace for table `test`.`t3` : Data structure corruption +ALTER TABLE t4 IMPORT TABLESPACE; +ERROR HY000: Schema mismatch (Expected FSP_SPACE_FLAGS=0x15, .ibd file contains 0x1010115.) +DROP TABLE t, t1, t2, t3, t4; +# Recovery from corruption only, no server restart +## t5: Recovery from corruption, with cfg +## t6: Recovery from corruption, without cfg +call mtr.add_suppression('InnoDB: ./test/t5.ibd: Page 0 at offset 0 looks corrupted.'); +call mtr.add_suppression("mariadbd.*: Index for table 't5' is corrupt; try to repair it"); +call mtr.add_suppression("InnoDB: Corrupted page \\[page id: space=.*, page number=0\\] of datafile './test/t5.ibd' could not be found in the doublewrite buffer."); +call mtr.add_suppression("InnoDB: Corrupted page \\[page id: space=.*, page number=0\\] of datafile './test/t6.ibd' could not be found in the doublewrite buffer."); +call mtr.add_suppression("mariadbd.*: Index for table 't6' is corrupt; try to repair it"); +CREATE TABLE t (a int) ENGINE=InnoDB; +INSERT INTO t VALUES(42); +FLUSH TABLES t FOR EXPORT; +UNLOCK TABLES; +# corrupting the 0th page +ALTER TABLE t5 IMPORT TABLESPACE; +ERROR HY000: Internal error: Error importing tablespace for table `test`.`t5` : Data structure corruption +ALTER TABLE t6 IMPORT TABLESPACE; +ERROR HY000: Schema mismatch (Expected FSP_SPACE_FLAGS=0x15, .ibd file contains 0x1010115.) +DROP TABLE t, t5, t6; diff --git a/mysql-test/suite/innodb/r/import_run_once.result b/mysql-test/suite/innodb/r/import_run_once.result new file mode 100644 index 00000000000..e49340b8dfc --- /dev/null +++ b/mysql-test/suite/innodb/r/import_run_once.result @@ -0,0 +1,104 @@ +# +# MDEV-26137 ALTER TABLE IMPORT enhancement +# +# drop t1 before importing t2 +CREATE TABLE t1 (a int) ENGINE=InnoDB; +INSERT INTO t1 VALUES(42); +FLUSH TABLES t1 FOR EXPORT; +UNLOCK TABLES; +DROP TABLE t1; +ALTER TABLE t2 IMPORT TABLESPACE; +SHOW CREATE TABLE t2; +Table Create Table +t2 CREATE TABLE `t2` ( + `a` int(11) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci +SELECT * FROM t2; +a +42 +DROP TABLE t2; +# created t2 but did not discard tablespace +CREATE TABLE t1 (a int) ENGINE=InnoDB; +INSERT INTO t1 VALUES(42); +CREATE TABLE t2 LIKE t1; +FLUSH TABLES t1 FOR EXPORT; +UNLOCK TABLES; +DROP TABLE t1; +call mtr.add_suppression("InnoDB: Unable to import tablespace"); +ALTER TABLE t2 IMPORT TABLESPACE; +ERROR HY000: Tablespace for table 'test/t2' exists. Please DISCARD the tablespace before IMPORT +SHOW CREATE TABLE t2; +Table Create Table +t2 CREATE TABLE `t2` ( + `a` int(11) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci +SELECT * FROM t2; +a +DROP TABLE t2; +# attempt to import when there's no tablespace +ALTER TABLE t2 IMPORT TABLESPACE; +ERROR 42S02: Table 'test.t2' doesn't exist +# with index +CREATE TABLE t1 (a int, b varchar(50)) ENGINE=InnoDB; +CREATE UNIQUE INDEX ai ON t1 (a); +INSERT INTO t1 VALUES(42, "hello"); +FLUSH TABLES t1 FOR EXPORT; +UNLOCK TABLES; +ALTER TABLE t2 IMPORT TABLESPACE; +SHOW CREATE TABLE t2; +Table Create Table +t2 CREATE TABLE `t2` ( + `a` int(11) DEFAULT NULL, + `b` varchar(50) DEFAULT NULL, + UNIQUE KEY `ai` (`a`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci +SELECT * FROM t2; +a b +42 hello +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 ai 1 a A 1 NULL NULL YES BTREE NO +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 ai 1 a A 1 NULL NULL YES BTREE NO +DROP TABLE t1, t2; +# with virtual column index +CREATE TABLE t1 (a int, b int as (a * a)) ENGINE=InnoDB; +CREATE UNIQUE INDEX ai ON t1 (b); +INSERT INTO t1 VALUES(42, default); +FLUSH TABLES t1 FOR EXPORT; +UNLOCK TABLES; +ALTER TABLE t2 IMPORT TABLESPACE; +SHOW CREATE TABLE t2; +Table Create Table +t2 CREATE TABLE `t2` ( + `a` int(11) DEFAULT NULL, + `b` int(11) GENERATED ALWAYS AS (`a` * `a`) VIRTUAL, + UNIQUE KEY `ai` (`b`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci +SELECT * FROM t2; +a b +42 1764 +SELECT b FROM t2 USE INDEX (ai); +b +1764 +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 ai 1 b A 1 NULL NULL YES BTREE NO +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 ai 1 b A 1 NULL NULL YES BTREE NO +CHECK TABLE t2 EXTENDED; +Table Op Msg_type Msg_text +test.t2 check status OK +DROP TABLE t1, t2; +# with auto_increment +CREATE TABLE t1 (id INT PRIMARY KEY AUTO_INCREMENT, i2 INT, i1 INT)ENGINE=INNODB; +INSERT INTO t1 (i2) SELECT 4 FROM seq_1_to_1024; +FLUSH TABLE t1 FOR EXPORT; +UNLOCK TABLES; +ALTER TABLE t2 IMPORT TABLESPACE; +CHECK TABLE t2 EXTENDED; +Table Op Msg_type Msg_text +test.t2 check status OK +DROP TABLE t2, t1; diff --git a/mysql-test/suite/innodb/t/import.test b/mysql-test/suite/innodb/t/import.test new file mode 100644 index 00000000000..307d2b4064f --- /dev/null +++ b/mysql-test/suite/innodb/t/import.test @@ -0,0 +1,3 @@ +--source include/import_begin.inc +--copy_file $MYSQLD_DATADIR/test/t1.cfg $MYSQLD_DATADIR/test/t2.cfg +--source include/import_end.inc diff --git a/mysql-test/suite/innodb/t/import_no_cfg.test b/mysql-test/suite/innodb/t/import_no_cfg.test new file mode 100644 index 00000000000..d22814fef5d --- /dev/null +++ b/mysql-test/suite/innodb/t/import_no_cfg.test @@ -0,0 +1,4 @@ +# embedded server uses absolute path, causing result mismatch in warning messages +--source include/not_embedded.inc +--source include/import_begin.inc +--source include/import_end.inc diff --git a/mysql-test/suite/innodb/t/import_recovery.test b/mysql-test/suite/innodb/t/import_recovery.test new file mode 100644 index 00000000000..ebaa25ff97c --- /dev/null +++ b/mysql-test/suite/innodb/t/import_recovery.test @@ -0,0 +1,163 @@ +--source include/have_innodb.inc +--source include/have_debug.inc +--source include/not_embedded.inc + +--echo # +--echo # MDEV-26137 ALTER TABLE IMPORT enhancement +--echo # + +let MYSQLD_DATADIR = `SELECT @@datadir`; +let INNODB_PAGE_SIZE=`select @@innodb_page_size`; + +call mtr.add_suppression('InnoDB: Tablespace for table `test`.`t1` is set as discarded.'); +call mtr.add_suppression('InnoDB: Tablespace for table `test`.`t2` is set as discarded.'); +call mtr.add_suppression('InnoDB: Tablespace for table `test`.`t3` is set as discarded.'); +call mtr.add_suppression('InnoDB: ./test/t3.ibd: Page 0 at offset 0 looks corrupted.'); +call mtr.add_suppression("mariadbd.*: Index for table 't3' is corrupt; try to repair it"); +call mtr.add_suppression("InnoDB: Expected tablespace id \\d+ but found \\d+ in the file ./test/t3.ibd"); +# In Windows etc. +call mtr.add_suppression("InnoDB: Corrupted page \\[page id: space=.*, page number=0\\] of datafile './test/t3.ibd' could not be found in the doublewrite buffer."); +call mtr.add_suppression('InnoDB: Tablespace for table `test`.`t4` is set as discarded.'); +call mtr.add_suppression('InnoDB: ./test/t4.ibd: Page 0 at offset 0 looks corrupted.'); +call mtr.add_suppression("mariadbd.*: Index for table 't4' is corrupt; try to repair it"); +# In Windows etc. +call mtr.add_suppression("InnoDB: Corrupted page \\[page id: space=.*, page number=0\\] of datafile './test/t4.ibd' could not be found in the doublewrite buffer."); + +--echo # Recovery from crashes +--echo ## t1: Creation of stub succeeds; server crashes; second import attempt succeeds +--echo ## t2: Creation of stub succeeds; server crashes; drop table +--echo ## t3: Creation of stub succeeds; server crashes; ibd corrupted; second import attempt fails; drop table +--echo ## t4: Did not copy .cfg; creation of stub succeeds; server crashes; ibd corrupted; second import attempt fails; drop table +CREATE TABLE t (a int) ENGINE=InnoDB; +INSERT INTO t VALUES(42); +FLUSH TABLES t FOR EXPORT; +--copy_file $MYSQLD_DATADIR/test/t.cfg $MYSQLD_DATADIR/test/t1.cfg +--copy_file $MYSQLD_DATADIR/test/t.frm $MYSQLD_DATADIR/test/t1.frm +--copy_file $MYSQLD_DATADIR/test/t.ibd $MYSQLD_DATADIR/test/t1.ibd +--copy_file $MYSQLD_DATADIR/test/t.cfg $MYSQLD_DATADIR/test/t2.cfg +--copy_file $MYSQLD_DATADIR/test/t.frm $MYSQLD_DATADIR/test/t2.frm +--copy_file $MYSQLD_DATADIR/test/t.ibd $MYSQLD_DATADIR/test/t2.ibd +--copy_file $MYSQLD_DATADIR/test/t.cfg $MYSQLD_DATADIR/test/t3.cfg +--copy_file $MYSQLD_DATADIR/test/t.frm $MYSQLD_DATADIR/test/t3.frm +--copy_file $MYSQLD_DATADIR/test/t.ibd $MYSQLD_DATADIR/test/t3.ibd +--copy_file $MYSQLD_DATADIR/test/t.frm $MYSQLD_DATADIR/test/t4.frm +--copy_file $MYSQLD_DATADIR/test/t.ibd $MYSQLD_DATADIR/test/t4.ibd +UNLOCK TABLES; + +SET GLOBAL innodb_max_dirty_pages_pct_lwm=0.0; +SET GLOBAL innodb_max_dirty_pages_pct=0.0; + +let $wait_condition = +SELECT variable_value = 0 +FROM information_schema.global_status +WHERE variable_name = 'INNODB_BUFFER_POOL_PAGES_DIRTY'; +--source include/wait_condition.inc + +connect (hang1,localhost,root); +SET DEBUG_SYNC='ib_after_create_stub_for_import SIGNAL hung WAIT_FOR ever'; +send ALTER TABLE t1 IMPORT TABLESPACE; +connection default; +SET DEBUG_SYNC='now WAIT_FOR hung'; + +connect (hang2,localhost,root); +SET DEBUG_SYNC='ib_after_create_stub_for_import SIGNAL hung WAIT_FOR ever'; +send ALTER TABLE t2 IMPORT TABLESPACE; +connection default; +SET DEBUG_SYNC='now WAIT_FOR hung'; + +connect (hang3,localhost,root); +SET DEBUG_SYNC='ib_after_create_stub_for_import SIGNAL hung WAIT_FOR ever'; +send ALTER TABLE t3 IMPORT TABLESPACE; +connection default; +SET DEBUG_SYNC='now WAIT_FOR hung'; + +connect (hang4,localhost,root); +SET DEBUG_SYNC='ib_after_create_stub_for_import SIGNAL hung WAIT_FOR ever'; +send ALTER TABLE t4 IMPORT TABLESPACE; +connection default; +SET DEBUG_SYNC='now WAIT_FOR hung'; + +let $shutdown_timeout=0; +--source include/shutdown_mysqld.inc + +--echo # corrupting the 0th page +perl; +my $ps = $ENV{INNODB_PAGE_SIZE}; + +@tables= ('t3', 't4'); +foreach $table (@tables) { + my $file = "$ENV{MYSQLD_DATADIR}/test/$table.ibd"; + open(FILE, "+<$file") || die "Unable to open $file"; + binmode FILE; + sysseek(FILE, 0, 0) || die "Unable to seek $file\n"; + die "Unable to read $file" unless sysread(FILE, $page, $ps) == $ps; + # Replace all NUL bytes with SOH bytes. + $page =~ tr/\x0/\x1/; + sysseek(FILE, 0, 0) || die "Unable to seek $file\n"; + syswrite(FILE, $page, $ps)==$ps || die "Unable to write $file\n"; + close FILE or die "close"; +} +EOF + +--echo # Restart mysqld after the crash and reconnect. +--source include/start_mysqld.inc + +ALTER TABLE t1 IMPORT TABLESPACE; +SHOW CREATE TABLE t1; +SELECT * FROM t1; + +--error ER_INTERNAL_ERROR +ALTER TABLE t3 IMPORT TABLESPACE; + +--error ER_TABLE_SCHEMA_MISMATCH +ALTER TABLE t4 IMPORT TABLESPACE; + +DROP TABLE t, t1, t2, t3, t4; + +--echo # Recovery from corruption only, no server restart +--echo ## t5: Recovery from corruption, with cfg +--echo ## t6: Recovery from corruption, without cfg +call mtr.add_suppression('InnoDB: ./test/t5.ibd: Page 0 at offset 0 looks corrupted.'); +call mtr.add_suppression("mariadbd.*: Index for table 't5' is corrupt; try to repair it"); +# In Windows etc. +call mtr.add_suppression("InnoDB: Corrupted page \\[page id: space=.*, page number=0\\] of datafile './test/t5.ibd' could not be found in the doublewrite buffer."); +# In Windows etc. +call mtr.add_suppression("InnoDB: Corrupted page \\[page id: space=.*, page number=0\\] of datafile './test/t6.ibd' could not be found in the doublewrite buffer."); +call mtr.add_suppression("mariadbd.*: Index for table 't6' is corrupt; try to repair it"); + +CREATE TABLE t (a int) ENGINE=InnoDB; +INSERT INTO t VALUES(42); +FLUSH TABLES t FOR EXPORT; +--copy_file $MYSQLD_DATADIR/test/t.cfg $MYSQLD_DATADIR/test/t5.cfg +--copy_file $MYSQLD_DATADIR/test/t.frm $MYSQLD_DATADIR/test/t5.frm +--copy_file $MYSQLD_DATADIR/test/t.ibd $MYSQLD_DATADIR/test/t5.ibd +--copy_file $MYSQLD_DATADIR/test/t.frm $MYSQLD_DATADIR/test/t6.frm +--copy_file $MYSQLD_DATADIR/test/t.ibd $MYSQLD_DATADIR/test/t6.ibd +UNLOCK TABLES; + +--echo # corrupting the 0th page +perl; +my $ps = $ENV{INNODB_PAGE_SIZE}; + +@tables= ('t5', 't6'); +foreach $table (@tables) { + my $file = "$ENV{MYSQLD_DATADIR}/test/$table.ibd"; + open(FILE, "+<$file") || die "Unable to open $file"; + binmode FILE; + sysseek(FILE, 0, 0) || die "Unable to seek $file\n"; + die "Unable to read $file" unless sysread(FILE, $page, $ps) == $ps; + # Replace all NUL bytes with SOH bytes. + $page =~ tr/\x0/\x1/; + sysseek(FILE, 0, 0) || die "Unable to seek $file\n"; + syswrite(FILE, $page, $ps)==$ps || die "Unable to write $file\n"; + close FILE or die "close"; +} +EOF + +--error ER_INTERNAL_ERROR +ALTER TABLE t5 IMPORT TABLESPACE; + +--error ER_TABLE_SCHEMA_MISMATCH +ALTER TABLE t6 IMPORT TABLESPACE; + +DROP TABLE t, t5, t6; diff --git a/mysql-test/suite/innodb/t/import_run_once.test b/mysql-test/suite/innodb/t/import_run_once.test new file mode 100644 index 00000000000..867f6434fe7 --- /dev/null +++ b/mysql-test/suite/innodb/t/import_run_once.test @@ -0,0 +1,86 @@ +--source include/have_innodb.inc +--source include/have_sequence.inc +let $MYSQLD_DATADIR = `SELECT @@datadir`; + +--echo # +--echo # MDEV-26137 ALTER TABLE IMPORT enhancement +--echo # + +--echo # drop t1 before importing t2 +CREATE TABLE t1 (a int) ENGINE=InnoDB; +INSERT INTO t1 VALUES(42); +FLUSH TABLES t1 FOR EXPORT; +--copy_file $MYSQLD_DATADIR/test/t1.cfg $MYSQLD_DATADIR/test/t2.cfg +--copy_file $MYSQLD_DATADIR/test/t1.frm $MYSQLD_DATADIR/test/t2.frm +--copy_file $MYSQLD_DATADIR/test/t1.ibd $MYSQLD_DATADIR/test/t2.ibd +UNLOCK TABLES; +DROP TABLE t1; +ALTER TABLE t2 IMPORT TABLESPACE; +SHOW CREATE TABLE t2; +SELECT * FROM t2; +DROP TABLE t2; + +--echo # created t2 but did not discard tablespace +CREATE TABLE t1 (a int) ENGINE=InnoDB; +INSERT INTO t1 VALUES(42); +CREATE TABLE t2 LIKE t1; +FLUSH TABLES t1 FOR EXPORT; +--copy_file $MYSQLD_DATADIR/test/t1.cfg $MYSQLD_DATADIR/test/t2.cfg +UNLOCK TABLES; +DROP TABLE t1; +call mtr.add_suppression("InnoDB: Unable to import tablespace"); +--error ER_TABLESPACE_EXISTS +ALTER TABLE t2 IMPORT TABLESPACE; +SHOW CREATE TABLE t2; +SELECT * FROM t2; +DROP TABLE t2; + +--echo # attempt to import when there's no tablespace +--error ER_NO_SUCH_TABLE +ALTER TABLE t2 IMPORT TABLESPACE; + +--echo # with index +CREATE TABLE t1 (a int, b varchar(50)) ENGINE=InnoDB; +CREATE UNIQUE INDEX ai ON t1 (a); +INSERT INTO t1 VALUES(42, "hello"); +FLUSH TABLES t1 FOR EXPORT; +--copy_file $MYSQLD_DATADIR/test/t1.cfg $MYSQLD_DATADIR/test/t2.cfg +--copy_file $MYSQLD_DATADIR/test/t1.frm $MYSQLD_DATADIR/test/t2.frm +--copy_file $MYSQLD_DATADIR/test/t1.ibd $MYSQLD_DATADIR/test/t2.ibd +UNLOCK TABLES; +ALTER TABLE t2 IMPORT TABLESPACE; +SHOW CREATE TABLE t2; +SELECT * FROM t2; +SHOW INDEX FROM t1; +SHOW INDEX FROM t2; +DROP TABLE t1, t2; + +--echo # with virtual column index +CREATE TABLE t1 (a int, b int as (a * a)) ENGINE=InnoDB; +CREATE UNIQUE INDEX ai ON t1 (b); +INSERT INTO t1 VALUES(42, default); +FLUSH TABLES t1 FOR EXPORT; +--copy_file $MYSQLD_DATADIR/test/t1.cfg $MYSQLD_DATADIR/test/t2.cfg +--copy_file $MYSQLD_DATADIR/test/t1.frm $MYSQLD_DATADIR/test/t2.frm +--copy_file $MYSQLD_DATADIR/test/t1.ibd $MYSQLD_DATADIR/test/t2.ibd +UNLOCK TABLES; +ALTER TABLE t2 IMPORT TABLESPACE; +SHOW CREATE TABLE t2; +SELECT * FROM t2; +SELECT b FROM t2 USE INDEX (ai); +SHOW INDEX FROM t1; +SHOW INDEX FROM t2; +CHECK TABLE t2 EXTENDED; +DROP TABLE t1, t2; + +--echo # with auto_increment +CREATE TABLE t1 (id INT PRIMARY KEY AUTO_INCREMENT, i2 INT, i1 INT)ENGINE=INNODB; +INSERT INTO t1 (i2) SELECT 4 FROM seq_1_to_1024; +FLUSH TABLE t1 FOR EXPORT; +--copy_file $MYSQLD_DATADIR/test/t1.frm $MYSQLD_DATADIR/test/t2.frm +--copy_file $MYSQLD_DATADIR/test/t1.ibd $MYSQLD_DATADIR/test/t2.ibd +--copy_file $MYSQLD_DATADIR/test/t1.cfg $MYSQLD_DATADIR/test/t2.cfg +UNLOCK TABLES; +ALTER TABLE t2 IMPORT TABLESPACE; +CHECK TABLE t2 EXTENDED; +DROP TABLE t2, t1; diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 70ecb47e799..ded936cd6d8 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -7239,6 +7239,7 @@ alter_commands: Lex->m_sql_cmd= new (thd->mem_root) Sql_cmd_discard_import_tablespace( Sql_cmd_discard_import_tablespace::IMPORT_TABLESPACE); + Lex->create_info.add(DDL_options_st::OPT_IMPORT_TABLESPACE); if (unlikely(Lex->m_sql_cmd == NULL)) MYSQL_YYABORT; } diff --git a/sql/structs.h b/sql/structs.h index 5b43948a563..40dd216bb65 100644 --- a/sql/structs.h +++ b/sql/structs.h @@ -537,7 +537,8 @@ public: OPT_OR_REPLACE_SLAVE_GENERATED= 32,// REPLACE was added on slave, it was // not in the original query on master. OPT_IF_EXISTS= 64, - OPT_CREATE_SELECT= 128 // CREATE ... SELECT + OPT_CREATE_SELECT= 128, // CREATE ... SELECT + OPT_IMPORT_TABLESPACE= 256 // ALTER ... IMPORT TABLESPACE }; private: @@ -566,6 +567,7 @@ public: bool like() const { return m_options & OPT_LIKE; } bool if_exists() const { return m_options & OPT_IF_EXISTS; } bool is_create_select() const { return m_options & OPT_CREATE_SELECT; } + bool import_tablespace() const { return m_options & OPT_IMPORT_TABLESPACE; } void add(const DDL_options_st::Options other) { diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc index 723e8e809de..adec5f4da16 100644 --- a/storage/innobase/handler/ha_innodb.cc +++ b/storage/innobase/handler/ha_innodb.cc @@ -5230,7 +5230,8 @@ create_table_info_t::create_table_info_t( m_create_info(create_info), m_table_name(table_name), m_table(NULL), m_remote_path(remote_path), - m_innodb_file_per_table(file_per_table) + m_innodb_file_per_table(file_per_table), + m_creating_stub(thd_ddl_options(thd)->import_tablespace()) { } @@ -5852,8 +5853,22 @@ ha_innobase::open(const char* name, int, uint) DEBUG_SYNC(thd, "ib_open_after_dict_open"); - if (NULL == ib_table) { + if (UNIV_LIKELY(ib_table != nullptr)) { + } else if (thd_ddl_options(thd)->import_tablespace()) { + /* If the table does not exist and we are trying to + import, create a "stub" table similar to the effects + of CREATE TABLE followed by ALTER TABLE ... DISCARD + TABLESPACE. */ + HA_CREATE_INFO create_info; + if (int err = prepare_create_stub_for_import(thd, norm_name, + create_info)) + DBUG_RETURN(err); + create(norm_name, table, &create_info, true, nullptr); + DEBUG_SYNC(thd, "ib_after_create_stub_for_import"); + ib_table = open_dict_table(name, norm_name, is_part, + DICT_ERR_IGNORE_FK_NOKEY); + } else { if (is_part) { sql_print_error("Failed to open table %s.\n", norm_name); @@ -10612,6 +10627,10 @@ create_table_info_t::create_table_def() ? doc_id_col : n_cols - num_v; } + /* Assume the tablespace is not available until we are able to + import it.*/ + table->file_unreadable = m_creating_stub; + if (DICT_TF_HAS_DATA_DIR(m_flags)) { ut_a(strlen(m_remote_path)); @@ -11625,6 +11644,10 @@ index_bad: } } + /* If we are trying to import a tablespace, mark tablespace as + discarded. */ + m_flags2 |= ulint{m_creating_stub} << DICT_TF2_POS_DISCARDED; + row_type = m_create_info->row_type; if (zip_ssize && zip_allowed) { @@ -12773,6 +12796,7 @@ int create_table_info_t::create_table(bool create_fk) dict_table_get_all_fts_indexes(m_table, fts->indexes); } + create_fk&= !m_creating_stub; dberr_t err = create_fk ? create_foreign_keys() : DB_SUCCESS; if (err == DB_SUCCESS) { @@ -13177,6 +13201,9 @@ ha_innobase::create(const char *name, TABLE *form, HA_CREATE_INFO *create_info, } if (!error) + /* We can't possibly have foreign key information when creating a + stub table for importing .frm / .cfg / .ibd because it is not + stored in any of these files. */ error= info.create_table(own_trx); if (own_trx || (info.flags2() & DICT_TF2_TEMPORARY)) @@ -13199,7 +13226,11 @@ ha_innobase::create(const char *name, TABLE *form, HA_CREATE_INFO *create_info, if (!error) { - dict_stats_update(info.table(), DICT_STATS_EMPTY_TABLE); + /* Skip stats update when creating a stub table for importing, + as it is not needed and would report error due to the table + not being readable yet. */ + if (!info.creating_stub()) + dict_stats_update(info.table(), DICT_STATS_EMPTY_TABLE); if (!info.table()->is_temporary()) log_write_up_to(trx->commit_lsn, true); info.table()->release(); diff --git a/storage/innobase/handler/ha_innodb.h b/storage/innobase/handler/ha_innodb.h index aedc7e097c4..cef3e5f8e66 100644 --- a/storage/innobase/handler/ha_innodb.h +++ b/storage/innobase/handler/ha_innodb.h @@ -701,6 +701,8 @@ public: ulint flags2() const { return(m_flags2); } + bool creating_stub() const { return UNIV_UNLIKELY(m_creating_stub); } + /** Get trx. */ trx_t* trx() const { return(m_trx); } @@ -767,6 +769,9 @@ private: /** Table flags2 */ ulint m_flags2; + + /** Whether we are creating a stub table for importing. */ + const bool m_creating_stub; }; /** diff --git a/storage/innobase/handler/handler0alter.cc b/storage/innobase/handler/handler0alter.cc index cb6ee389ff1..c0d6c2fe13e 100644 --- a/storage/innobase/handler/handler0alter.cc +++ b/storage/innobase/handler/handler0alter.cc @@ -10200,7 +10200,16 @@ commit_try_rebuild( "parent" table. */ if (!user_table->space) { rebuilt_table->file_unreadable = true; +#if defined __GNUC__ && !defined __clang__ +# pragma GCC diagnostic push +# if __GNUC__ < 12 || defined WITH_UBSAN +# pragma GCC diagnostic ignored "-Wconversion" +# endif +#endif rebuilt_table->flags2 |= DICT_TF2_DISCARDED; +#if defined __GNUC__ && !defined __clang__ +# pragma GCC diagnostic pop +#endif } /* We can now rename the old table as a temporary table, diff --git a/storage/innobase/include/dict0mem.h b/storage/innobase/include/dict0mem.h index c1927bec916..5d6b5e13ddd 100644 --- a/storage/innobase/include/dict0mem.h +++ b/storage/innobase/include/dict0mem.h @@ -264,7 +264,8 @@ use its own tablespace instead of the system tablespace. */ #define DICT_TF2_USE_FILE_PER_TABLE 16U /** Set when we discard/detach the tablespace */ -#define DICT_TF2_DISCARDED 32U +constexpr unsigned DICT_TF2_POS_DISCARDED= 5; +constexpr unsigned DICT_TF2_DISCARDED= 1U << DICT_TF2_POS_DISCARDED; /** This bit is set if all aux table names (both common tables and index tables) of a FTS table are in HEX format. */ @@ -2097,8 +2098,9 @@ public: process of altering partitions */ unsigned skip_alter_undo:1; - /*!< whether this is in a single-table tablespace and the .ibd - file is missing or page decryption failed and page is corrupted */ + /** whether this is in a single-table tablespace and the .ibd file + is believed to be missing or page decryption failed and page is + corrupted */ unsigned file_unreadable:1; /** TRUE if the table object has been added to the dictionary cache. */ diff --git a/storage/innobase/include/row0import.h b/storage/innobase/include/row0import.h index fd2651da39e..33e0da0ffe3 100644 --- a/storage/innobase/include/row0import.h +++ b/storage/innobase/include/row0import.h @@ -33,6 +33,7 @@ Created 2012-02-08 by Sunny Bains struct trx_t; struct dict_table_t; struct row_prebuilt_t; +struct HA_CREATE_INFO; /*****************************************************************//** Imports a tablespace. The space id in the .ibd file must match the space id @@ -64,4 +65,13 @@ dberr_t row_import_update_index_root(trx_t* trx, dict_table_t* table, bool reset) MY_ATTRIBUTE((nonnull, warn_unused_result)); +/** Prepare the create info to create a new stub table for import. +@param thd Connection +@param name Table name, format: "db/table_name". +@param create_info The create info for creating a stub. +@return ER_ error code +@retval 0 on success */ +int prepare_create_stub_for_import(THD *thd, const char *name, + HA_CREATE_INFO& create_info); + #endif /* row0import_h */ diff --git a/storage/innobase/row/row0import.cc b/storage/innobase/row/row0import.cc index 40fd8c75be2..065defb4830 100644 --- a/storage/innobase/row/row0import.cc +++ b/storage/innobase/row/row0import.cc @@ -686,7 +686,8 @@ struct FetchIndexRootPages : public AbstractCallback { /** Constructor @param trx covering (user) transaction @param table table definition in server .*/ - FetchIndexRootPages(const dict_table_t* table, trx_t* trx) + FetchIndexRootPages(const dict_table_t* table = nullptr, + trx_t* trx = nullptr) : AbstractCallback(trx, UINT32_MAX), m_table(table), m_index(0, 0) UNIV_NOTHROW { } @@ -701,18 +702,46 @@ struct FetchIndexRootPages : public AbstractCallback { dberr_t run(const fil_iterator_t& iter, buf_block_t* block) UNIV_NOTHROW override; - /** Called for each block as it is read from the file. + /** Check that fsp flags and row formats match. @param block block to convert, it is not from the buffer pool. @retval DB_SUCCESS or error code. */ dberr_t operator()(buf_block_t* block) UNIV_NOTHROW override; + /** Get row format from the header and the root index page. */ + enum row_type get_row_format(const buf_block_t &block) + { + if (!page_is_comp(block.page.frame)) + return ROW_TYPE_REDUNDANT; + /* With full_crc32 we cannot tell between dynamic or + compact, and return not_used. We cannot simply return + dynamic or compact, as the client of this function + will not be able to tell whether it is dynamic because + of this or the other branch below. Returning default + would also work if it is immediately handled, but is + still more ambiguous than not_used, which is not a + row_format at all. */ + if (fil_space_t::full_crc32(m_space_flags)) + return ROW_TYPE_NOT_USED; + if (!(m_space_flags & FSP_FLAGS_MASK_ATOMIC_BLOBS)) + return ROW_TYPE_COMPACT; + if (FSP_FLAGS_GET_ZIP_SSIZE(m_space_flags)) + return ROW_TYPE_COMPRESSED; + return ROW_TYPE_DYNAMIC; + } + /** Update the import configuration that will be used to import the tablespace. */ dberr_t build_row_import(row_import* cfg) const UNIV_NOTHROW; - /** Table definition in server. */ + /** Table definition in server. When the table is being + created, there's no table yet so m_table is nullptr */ const dict_table_t* m_table; + /** Table row format. Only used when a (stub) table is being + created in which case m_table is null, for obtaining row + format from the .ibd for the stub table. */ + enum row_type m_row_format; + /** Index information */ Index m_index; }; @@ -3353,6 +3382,35 @@ static dberr_t handle_instant_metadata(dict_table_t *table, return DB_SUCCESS; } +/** +Read the contents of a .cfg file. +@param[in] filename Path to the cfg file +@param[in] thd Connection +@param[out] cfg Contents of the .cfg file. +@return DB_SUCCESS or error code. */ +static dberr_t row_import_read_cfg_internal(const char *filename, THD *thd, + row_import &cfg) +{ + FILE *file= fopen(filename, "rb"); + + cfg.m_missing= !file; + + if (!file) + { + char msg[BUFSIZ]; + snprintf(msg, sizeof(msg), + "Error opening '%s', will attempt to import" + " without schema verification", filename); + ib_senderrf(thd, IB_LOG_LEVEL_WARN, ER_IO_READ_ERROR, + (ulong) errno, strerror(errno), msg); + return DB_FAIL; + } + + dberr_t err= row_import_read_meta_data(file, thd, cfg); + fclose(file); + return err; +} + /** Read the contents of the .cfg file. @return DB_SUCCESS or error code. */ @@ -3364,38 +3422,60 @@ row_import_read_cfg( THD* thd, /*!< in: session */ row_import& cfg) /*!< out: contents of the .cfg file */ { - dberr_t err; char name[OS_FILE_MAX_PATH]; cfg.m_table = table; srv_get_meta_data_filename(table, name, sizeof(name)); - FILE* file = fopen(name, "rb"); + return row_import_read_cfg_internal(name, thd, cfg); +} - if (file == NULL) { - char msg[BUFSIZ]; - snprintf(msg, sizeof(msg), - "Error opening '%s', will attempt to import" - " without schema verification", name); +/** Convert the InnoDB ROW_FORMAT from rec_format_enum to row_type. +@param[in] from ROW_FORMAT as a rec_format_enum +@return the row_type representation of ROW_FORMAT. */ +static enum row_type from_rec_format(const rec_format_enum from) +{ + switch (from) { + case REC_FORMAT_COMPACT: + return ROW_TYPE_COMPACT; + case REC_FORMAT_DYNAMIC: + return ROW_TYPE_DYNAMIC; + case REC_FORMAT_REDUNDANT: + return ROW_TYPE_REDUNDANT; + case REC_FORMAT_COMPRESSED: + return ROW_TYPE_COMPRESSED; + } - ib_senderrf( - thd, IB_LOG_LEVEL_WARN, ER_IO_READ_ERROR, - (ulong) errno, strerror(errno), msg); + ut_ad("invalid format" == 0); + return ROW_TYPE_NOT_USED; +} - cfg.m_missing = true; - - err = DB_FAIL; - } else { - - cfg.m_missing = false; - - err = row_import_read_meta_data(file, thd, cfg); - fclose(file); - } - - return(err); +/** +Read the row type from a .cfg file. +@param dir_path Path to the data directory containing the .cfg file +@param name Name of the table +@param thd Connection +@retval ROW_TYPE_COMPACT for ROW_FORMAT=COMPACT +@retval ROW_TYPE_DYNAMIC for ROW_FORMAT=DYNAMIC +@retval ROW_TYPE_REDUNDANT for ROW_FORMAT=REDUNDANT +@retval ROW_TYPE_COMPRESSED for ROW_FORMAT=COMPRESSED +@retval ROW_TYPE_NOT_USED to signal error */ +static enum row_type get_row_type_from_cfg(const char* dir_path, + const char* name, THD* thd) +{ + char* filename= fil_make_filepath(dir_path, + table_name_t(const_cast(name)), + CFG, dir_path != nullptr); + if (!filename) + return ROW_TYPE_NOT_USED; + row_import cfg; + dberr_t err= row_import_read_cfg_internal(filename, thd, cfg); + ut_free(filename); + if (err == DB_SUCCESS) + return from_rec_format(dict_tf_get_rec_format(cfg.m_flags)); + return ROW_TYPE_NOT_USED; } /** Update the root page numbers and tablespace ID of a table. @@ -3528,11 +3608,20 @@ row_import_set_discarded( ulint flags2 = mach_read_from_4( static_cast(dfield_get_data(dfield))); +#if defined __GNUC__ && !defined __clang__ +# pragma GCC diagnostic push +# if __GNUC__ < 12 || defined WITH_UBSAN +# pragma GCC diagnostic ignored "-Wconversion" +# endif +#endif if (discard->state) { flags2 |= DICT_TF2_DISCARDED; } else { flags2 &= ~DICT_TF2_DISCARDED; } +#if defined __GNUC__ && !defined __clang__ +# pragma GCC diagnostic pop +#endif mach_write_to_4(reinterpret_cast(&discard->flags2), flags2); @@ -3741,7 +3830,12 @@ page_corrupted: && buf_page_is_corrupted(false, readptr, m_space_flags)) goto page_corrupted; - err= this->operator()(block); + /* m_table is null iff we are trying to create a (stub) table, in + which case we want to get row format for the table creation. */ + if (m_table) + err= this->operator()(block); + else + m_row_format= get_row_format(*block); func_exit: free(page_compress_buf); return err; @@ -4064,19 +4158,21 @@ func_exit: return err; } -/********************************************************************//** -Iterate over all the pages in the tablespace. -@param table - the table definiton in the server -@param n_io_buffers - number of blocks to read and write together -@param callback - functor that will do the page updates +/** +Iterate over all or some pages in the tablespace. +@param dir_path the path to data dir storing the tablespace +@param name the table name +@param n_io_buffers number of blocks to read and write together +@param callback functor that will do the page queries or updates @return DB_SUCCESS or error code */ static dberr_t fil_tablespace_iterate( /*===================*/ - dict_table_t* table, - ulint n_io_buffers, - AbstractCallback& callback) + const char *name, + ulint n_io_buffers, + AbstractCallback &callback, + const char *dir_path) { dberr_t err; pfs_os_file_t file; @@ -4088,18 +4184,9 @@ fil_tablespace_iterate( DBUG_EXECUTE_IF("ib_import_trigger_corruption_1", return(DB_CORRUPTION);); - /* Make sure the data_dir_path is set. */ - dict_get_and_save_data_dir_path(table); - - ut_ad(!DICT_TF_HAS_DATA_DIR(table->flags) || table->data_dir_path); - - const char *data_dir_path = DICT_TF_HAS_DATA_DIR(table->flags) - ? table->data_dir_path : nullptr; - - filepath = fil_make_filepath(data_dir_path, - {table->name.m_name, - strlen(table->name.m_name)}, - IBD, data_dir_path != nullptr); + table_name_t table_name(const_cast(name)); + filepath= fil_make_filepath(dir_path, table_name, IBD, + dir_path != nullptr); if (!filepath) { return(DB_OUT_OF_MEMORY); } else { @@ -4112,9 +4199,9 @@ fil_tablespace_iterate( if (!success) { /* The following call prints an error message */ os_file_get_last_error(true); - ib::error() << "Trying to import a tablespace," - " but could not open the tablespace file " - << filepath; + sql_print_error("InnoDB: could not open the " + "tablespace file %s.\n", + filepath); ut_free(filepath); return DB_TABLESPACE_NOT_FOUND; } else { @@ -4231,6 +4318,24 @@ fil_tablespace_iterate( return(err); } +/** +Iterate over all or some pages in the tablespace. +@param table the table definiton in the server +@param n_io_buffers number of blocks to read and write together +@param callback functor that will do the page queries or updates +@return DB_SUCCESS or error code */ +static dberr_t fil_tablespace_iterate(dict_table_t *table, ulint n_io_buffers, + AbstractCallback &callback) +{ + /* Make sure the data_dir_path is set. */ + dict_get_and_save_data_dir_path(table); + ut_ad(!DICT_TF_HAS_DATA_DIR(table->flags) || table->data_dir_path); + const char *data_dir_path= DICT_TF_HAS_DATA_DIR(table->flags) + ? table->data_dir_path : nullptr; + return fil_tablespace_iterate(table->name.m_name, n_io_buffers, callback, + data_dir_path); +} + /*****************************************************************//** Imports a tablespace. The space id in the .ibd file must match the space id of the table in the data dictionary. @@ -4564,3 +4669,58 @@ row_import_for_mysql( return row_import_cleanup(prebuilt, err); } + +/** Prepare the create info to create a new stub table for import. +@param thd Connection +@param name Table name, format: "db/table_name". +@param create_info The create info for creating a stub. +@return ER_ error code +@retval 0 on success */ +int prepare_create_stub_for_import(THD *thd, const char *name, + HA_CREATE_INFO& create_info) +{ + DBUG_ENTER("prepare_create_stub_for_import"); + FetchIndexRootPages fetchIndexRootPages; + if (fil_tablespace_iterate(name, IO_BUFFER_SIZE(srv_page_size), + fetchIndexRootPages, fil_path_to_mysql_datadir) + != DB_SUCCESS) + { + const char *ibd_path= fil_make_filepath( + fil_path_to_mysql_datadir, table_name_t(const_cast(name)), IBD, + true); + if (!ibd_path) + return(ER_ENGINE_OUT_OF_MEMORY); + sql_print_error("InnoDB: failed to get row format from %s.\n", + ibd_path); + DBUG_RETURN(ER_INNODB_IMPORT_ERROR); + } + create_info.init(); + /* get the row format from ibd. */ + create_info.row_type= fetchIndexRootPages.m_row_format; + /* if .cfg exists, get the row format from cfg, and compare with + ibd, report error if different, except when cfg reports + compact/dynamic and ibd reports not_used (indicating either compact + or dynamic but not sure) */ + const enum row_type row_type_from_cfg= + get_row_type_from_cfg(fil_path_to_mysql_datadir, name, thd); + if (row_type_from_cfg != ROW_TYPE_NOT_USED) + { + /* if ibd reports not_used but cfg reports compact or dynamic, go + with cfg. */ + if (create_info.row_type != row_type_from_cfg && + !((row_type_from_cfg == ROW_TYPE_COMPACT || + row_type_from_cfg == ROW_TYPE_DYNAMIC) && + create_info.row_type == ROW_TYPE_NOT_USED)) + { + sql_print_error( + "InnoDB: cfg and ibd disagree on row format for table %s.\n", + name); + DBUG_RETURN(ER_INNODB_IMPORT_ERROR); + } + else + create_info.row_type= row_type_from_cfg; + } + else if (create_info.row_type == ROW_TYPE_NOT_USED) + create_info.row_type= ROW_TYPE_DYNAMIC; + DBUG_RETURN(0); +} diff --git a/storage/innobase/row/row0mysql.cc b/storage/innobase/row/row0mysql.cc index 27f48c1a4de..2b8909a7cf8 100644 --- a/storage/innobase/row/row0mysql.cc +++ b/storage/innobase/row/row0mysql.cc @@ -2449,7 +2449,16 @@ rollback: ALTER TABLE...DISCARD TABLESPACE operation altogether. */ table->file_unreadable= true; table->space= nullptr; +#if defined __GNUC__ && !defined __clang__ +# pragma GCC diagnostic push +# if __GNUC__ < 12 || defined WITH_UBSAN +# pragma GCC diagnostic ignored "-Wconversion" +# endif +#endif table->flags2|= DICT_TF2_DISCARDED; +#if defined __GNUC__ && !defined __clang__ +# pragma GCC diagnostic pop +#endif err= row_discard_tablespace(trx, table); DBUG_EXECUTE_IF("ib_discard_before_commit_crash", log_buffer_flush_to_disk(); DBUG_SUICIDE(););