From df55eabe73167fda04d3f513fc1ac216d3ca38ef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= <marko.makela@mariadb.com>
Date: Fri, 19 Jan 2018 17:41:57 +0200
Subject: [PATCH 3/4] MDEV-14941 Timeouts on persistent statistics tables after
 MDEV-14511

MDEV-14511 tries to avoid some consistency problems related to InnoDB
persistent statistics. The persistent statistics are being written by
an InnoDB internal SQL interpreter that requires the InnoDB
data dictionary cache to be locked.

Before MDEV-14511, the statistics were written during DDL in separate
transactions, which could unnecessarily reduce performance (each commit
would require a redo log flush) and break atomicity, because the
statistics would be updated separately from the dictionary transaction.

However, because it is unacceptable to hold the InnoDB data dictionary
cache locked while suspending the execution for waiting for a
transactional lock (in the mysql.innodb_index_stats or
mysql.innodb_table_stats tables) to be released, any lock conflict
was immediately reported as "lock wait timeout".

MySQL 5.7.11 (and MariaDB 10.2.2) will purposely drop the statistics
when adding or dropping virtual columns in ALTER TABLE...ALGORITHM=INPLACE.
It turns out that we can do the same with ALTER TABLE...ALGORITHM=COPY
and with table-rebuilding and DROP INDEX operations performed by
ALGORITHM=INPLACE. The statistics would be recalculated when the table
is next opened.

With this fix, it is still possible to get statistics-related error
messages on ALTER TABLE operations on partitioned tables, such as
ALTER TABLE t1 PARTITION BY HASH(pk). In my test, this was due to
a locking conflict between dict_stats_thread executing dict_stats_update()
and the ALTER TABLE renaming one of the partitions.

DB_TABLE_IN_FK_CHECK, HA_ERR_TABLE_IN_FK_CHECK: Remove. A RENAME TABLE
operation will no longer conflict with FOREIGN KEY checks, because
it will acquire an exclusive lock on the table.

dict_stats_snapshot_create(), dict_stats_snapshot_free(): Remove.
The table object cannot disappear, because we will hold a table lock.

dict_stats_update_persistent(): Acquire a table IS lock to prevent
a conflict on statistics updates in RENAME TABLE.

dict_stats_save(): Acquire a table IS lock to prevent
a conflict on statistics updates in RENAME TABLE. (Empty statistics
are written without invoking dict_stats_update_persistent().)

dict_stats_drop_index(): Acquire dict_sys->mutex for a shorter period,
and let the caller report a more compact error.

dict_stats_drop_table(): Let the caller report a more compact error.

dict_stats_rename_table(): Remove some retrying logic. If a timeout
occurs (which should be rare, because we will now hold an exclusive
lock on the table), there is no point sleeping and retrying.

innobase_rename_table(): Do not lock the data dictionary, and do
not update the statistics. That is now done in
row_rename_table_for_mysql().

drop_stats(): Drop persistent statistics during certain
ALTER TABLE...ALGORITHM=INPLACE operations.

alter_stats_rebuild(): Remove.

row_mysql_lock_table(): Allow LOCK_IS to be acquired.

lock_table_enqueue_waiting(): Relax the condition about no lock
waits during a dictionary operation.
---
 include/handler_ername.h                           |   1 -
 include/my_base.h                                  |   1 -
 .../suite/innodb/r/foreign_key_locking.result      |  45 ++++
 .../suite/innodb/r/innodb_stats_drop_locked.result |   7 +-
 mysql-test/suite/innodb/t/foreign_key_locking.test |  47 ++++
 .../suite/innodb/t/innodb_stats_drop_locked.test   |  14 +-
 sql/handler.cc                                     |   4 -
 storage/innobase/dict/dict0stats.cc                | 279 ++++-----------------
 storage/innobase/handler/ha_innodb.cc              |  46 +---
 storage/innobase/handler/handler0alter.cc          | 215 +++++-----------
 storage/innobase/include/db0err.h                  |   4 +-
 storage/innobase/include/dict0stats.h              |  12 -
 storage/innobase/include/row0mysql.h               |  16 +-
 storage/innobase/lock/lock0lock.cc                 |  10 +-
 storage/innobase/row/row0mysql.cc                  | 104 +++++---
 storage/innobase/row/row0trunc.cc                  |  10 +-
 storage/innobase/ut/ut0ut.cc                       |   4 +-
 17 files changed, 304 insertions(+), 515 deletions(-)
 create mode 100644 mysql-test/suite/innodb/r/foreign_key_locking.result
 create mode 100644 mysql-test/suite/innodb/t/foreign_key_locking.test

diff --git a/include/handler_ername.h b/include/handler_ername.h
index 50f7f535806..1c34bc10038 100644
--- a/include/handler_ername.h
+++ b/include/handler_ername.h
@@ -74,7 +74,6 @@
 { "HA_ERR_INDEX_COL_TOO_LONG", HA_ERR_INDEX_COL_TOO_LONG, "" },
 { "HA_ERR_INDEX_CORRUPT", HA_ERR_INDEX_CORRUPT, "" },
 { "HA_ERR_UNDO_REC_TOO_BIG", HA_ERR_UNDO_REC_TOO_BIG, "" },
-{ "HA_ERR_TABLE_IN_FK_CHECK", HA_ERR_TABLE_IN_FK_CHECK, "" },
 { "HA_ERR_ROW_NOT_VISIBLE", HA_ERR_ROW_NOT_VISIBLE, "" },
 { "HA_ERR_ABORTED_BY_USER", HA_ERR_ABORTED_BY_USER, "" },
 { "HA_ERR_DISK_FULL", HA_ERR_DISK_FULL, "" },
diff --git a/include/my_base.h b/include/my_base.h
index 89f5e826fd5..74dbc1f0589 100644
--- a/include/my_base.h
+++ b/include/my_base.h
@@ -488,7 +488,6 @@ enum ha_base_keytype {
 #define HA_ERR_INDEX_CORRUPT      180    /* Index corrupted */
 #define HA_ERR_UNDO_REC_TOO_BIG   181    /* Undo log record too big */
 #define HA_FTS_INVALID_DOCID      182	 /* Invalid InnoDB Doc ID */
-#define HA_ERR_TABLE_IN_FK_CHECK  183    /* Table being used in foreign key check */
 #define HA_ERR_TABLESPACE_EXISTS  184    /* The tablespace existed in storage engine */
 #define HA_ERR_TOO_MANY_FIELDS    185    /* Table has too many columns */
 #define HA_ERR_ROW_IN_WRONG_PARTITION 186 /* Row in wrong partition */
diff --git a/mysql-test/suite/innodb/r/foreign_key_locking.result b/mysql-test/suite/innodb/r/foreign_key_locking.result
new file mode 100644
index 00000000000..f93424cacab
--- /dev/null
+++ b/mysql-test/suite/innodb/r/foreign_key_locking.result
@@ -0,0 +1,45 @@
+CREATE TABLE t1 (id INT NOT NULL PRIMARY KEY) ENGINE=InnoDB;
+CREATE TABLE t2 (
+id INT NOT NULL PRIMARY KEY,
+ref_id INT NOT NULL DEFAULT 0,
+f INT NULL,
+FOREIGN KEY (ref_id) REFERENCES t1 (id) ON DELETE CASCADE
+) ENGINE=InnoDB;
+INSERT INTO t1 VALUES (1),(2);
+INSERT INTO t2 VALUES (1,1,10),(2,2,20);
+connect  con1,localhost,root,,;
+BEGIN;
+UPDATE t2 SET ref_id = 2 WHERE id = 1;
+connection default;
+SET innodb_lock_wait_timeout=1;
+SET lock_wait_timeout=2;
+RENAME TABLE t1 TO t3;
+ERROR HY000: Error on rename of './test/t1' to './test/t3' (errno: 146 "Lock timed out; Retry transaction")
+UPDATE t1 SET id = id + 2;
+ERROR HY000: Lock wait timeout exceeded; try restarting transaction
+ALTER TABLE t1 FORCE, ALGORITHM=COPY;
+ERROR HY000: Error on rename of './test/t1' to './test/#sql2-temporary' (errno: 146 "Lock timed out; Retry transaction")
+ALTER TABLE t1 FORCE, ALGORITHM=INPLACE;
+ERROR HY000: Lock wait timeout exceeded; try restarting transaction
+ALTER TABLE t2 FORCE, ALGORITHM=COPY;
+ERROR HY000: Lock wait timeout exceeded; try restarting transaction
+ALTER TABLE t2 FORCE, ALGORITHM=INPLACE;
+ERROR HY000: Lock wait timeout exceeded; try restarting transaction
+connection con1;
+COMMIT;
+disconnect con1;
+connection default;
+SELECT * FROM t1;
+id
+1
+2
+SELECT * FROM t2;
+id	ref_id	f
+1	2	10
+2	2	20
+DELETE FROM t1 WHERE id = 1;
+SELECT * FROM t2;
+id	ref_id	f
+1	2	10
+2	2	20
+DROP TABLE t2, t1;
diff --git a/mysql-test/suite/innodb/r/innodb_stats_drop_locked.result b/mysql-test/suite/innodb/r/innodb_stats_drop_locked.result
index b8f312ccd63..91f95f8c6f7 100644
--- a/mysql-test/suite/innodb/r/innodb_stats_drop_locked.result
+++ b/mysql-test/suite/innodb/r/innodb_stats_drop_locked.result
@@ -1,6 +1,9 @@
+CREATE TABLE innodb_stats_drop_locked (c INT, KEY c_key (c))
+ENGINE=INNODB STATS_PERSISTENT=1;
+ANALYZE TABLE innodb_stats_drop_locked;
 Table	Op	Msg_type	Msg_text
 test.innodb_stats_drop_locked	analyze	status	OK
-SET autocommit=0;
+BEGIN;
 SELECT table_name FROM mysql.innodb_table_stats
 WHERE table_name='innodb_stats_drop_locked'
 FOR UPDATE;
@@ -21,7 +24,7 @@ connect  con1,localhost,root,,;
 connection con1;
 ALTER TABLE innodb_stats_drop_locked DROP INDEX c_key;
 Warnings:
-Warning	1205	Unable to delete statistics for index c_key from mysql.innodb_index_stats because the rows are locked: Lock wait timeout. They can be deleted later using DELETE FROM mysql.innodb_index_stats WHERE database_name = 'test' AND table_name = 'innodb_stats_drop_locked' AND index_name = 'c_key';
+Warning	1088	ALTER TABLE failed to adjust statistics for table 'innodb_stats_drop_locked'. Run ANALYZE TABLE.
 SHOW CREATE TABLE innodb_stats_drop_locked;
 Table	Create Table
 innodb_stats_drop_locked	CREATE TABLE `innodb_stats_drop_locked` (
diff --git a/mysql-test/suite/innodb/t/foreign_key_locking.test b/mysql-test/suite/innodb/t/foreign_key_locking.test
new file mode 100644
index 00000000000..eb6183c3142
--- /dev/null
+++ b/mysql-test/suite/innodb/t/foreign_key_locking.test
@@ -0,0 +1,47 @@
+--source include/have_innodb.inc
+
+CREATE TABLE t1 (id INT NOT NULL PRIMARY KEY) ENGINE=InnoDB;
+
+CREATE TABLE t2 (
+  id INT NOT NULL PRIMARY KEY,
+  ref_id INT NOT NULL DEFAULT 0,
+  f INT NULL,
+  FOREIGN KEY (ref_id) REFERENCES t1 (id) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+INSERT INTO t1 VALUES (1),(2);
+INSERT INTO t2 VALUES (1,1,10),(2,2,20);
+
+--connect (con1,localhost,root,,)
+BEGIN;
+UPDATE t2 SET ref_id = 2 WHERE id = 1;
+
+--connection default
+SET innodb_lock_wait_timeout=1;
+SET lock_wait_timeout=2;
+
+--error ER_ERROR_ON_RENAME
+RENAME TABLE t1 TO t3;
+--error ER_LOCK_WAIT_TIMEOUT
+UPDATE t1 SET id = id + 2;
+--replace_regex /#sql2-.*'/#sql2-temporary'/
+--error ER_ERROR_ON_RENAME
+ALTER TABLE t1 FORCE, ALGORITHM=COPY;
+--error ER_LOCK_WAIT_TIMEOUT
+ALTER TABLE t1 FORCE, ALGORITHM=INPLACE;
+--error ER_LOCK_WAIT_TIMEOUT
+ALTER TABLE t2 FORCE, ALGORITHM=COPY;
+--error ER_LOCK_WAIT_TIMEOUT
+ALTER TABLE t2 FORCE, ALGORITHM=INPLACE;
+
+--connection con1
+COMMIT;
+--disconnect con1
+
+--connection default
+SELECT * FROM t1;
+SELECT * FROM t2;
+DELETE FROM t1 WHERE id = 1;
+SELECT * FROM t2;
+
+DROP TABLE t2, t1;
diff --git a/mysql-test/suite/innodb/t/innodb_stats_drop_locked.test b/mysql-test/suite/innodb/t/innodb_stats_drop_locked.test
index 47f363a4bb6..903acc08736 100644
--- a/mysql-test/suite/innodb/t/innodb_stats_drop_locked.test
+++ b/mysql-test/suite/innodb/t/innodb_stats_drop_locked.test
@@ -5,20 +5,12 @@
 
 -- source include/have_innodb.inc
 
--- disable_warnings
--- disable_query_log
-
-DROP TABLE IF EXISTS innodb_stats_drop_locked;
-
 CREATE TABLE innodb_stats_drop_locked (c INT, KEY c_key (c))
 ENGINE=INNODB STATS_PERSISTENT=1;
 
 ANALYZE TABLE innodb_stats_drop_locked;
 
--- enable_warnings
--- enable_query_log
-
-SET autocommit=0;
+BEGIN;
 
 SELECT table_name FROM mysql.innodb_table_stats
 WHERE table_name='innodb_stats_drop_locked'
@@ -43,7 +35,6 @@ DROP TABLE innodb_stats_drop_locked;
 SHOW TABLES;
 
 -- connection default
-
 -- disconnect con1
 
 COMMIT;
@@ -57,5 +48,6 @@ SELECT table_name FROM mysql.innodb_index_stats
 WHERE table_name='innodb_stats_drop_locked';
 
 --disable_query_log
-call mtr.add_suppression("Unable to delete statistics for table test\\.innodb_stats_drop_locked: Lock wait");
+call mtr.add_suppression("InnoDB: ALTER TABLE failed to adjust statistics for table 'innodb_stats_drop_locked'");
+call mtr.add_suppression("InnoDB: DROP TABLE `test`.`innodb_stats_drop_locked` failed to drop statistics");
 --enable_query_log
diff --git a/sql/handler.cc b/sql/handler.cc
index 7eed722a971..2ef488b6171 100644
--- a/sql/handler.cc
+++ b/sql/handler.cc
@@ -375,7 +375,6 @@ int ha_init_errors(void)
   SETMSG(HA_ERR_INDEX_COL_TOO_LONG,	ER_DEFAULT(ER_INDEX_COLUMN_TOO_LONG));
   SETMSG(HA_ERR_INDEX_CORRUPT,		ER_DEFAULT(ER_INDEX_CORRUPT));
   SETMSG(HA_FTS_INVALID_DOCID,		"Invalid InnoDB FTS Doc ID");
-  SETMSG(HA_ERR_TABLE_IN_FK_CHECK,	ER_DEFAULT(ER_TABLE_IN_FK_CHECK));
   SETMSG(HA_ERR_DISK_FULL,              ER_DEFAULT(ER_DISK_FULL));
   SETMSG(HA_ERR_FTS_TOO_MANY_WORDS_IN_PHRASE,  "Too many words in a FTS phrase or proximity search");
   SETMSG(HA_ERR_FK_DEPTH_EXCEEDED,      "Foreign key cascade delete/update exceeds");
@@ -3612,9 +3611,6 @@ void handler::print_error(int error, myf errflag)
   case HA_ERR_UNDO_REC_TOO_BIG:
     textno= ER_UNDO_RECORD_TOO_BIG;
     break;
-  case HA_ERR_TABLE_IN_FK_CHECK:
-    textno= ER_TABLE_IN_FK_CHECK;
-    break;
   default:
     {
       /* The error was "unknown" to this function.
diff --git a/storage/innobase/dict/dict0stats.cc b/storage/innobase/dict/dict0stats.cc
index 109c5241278..e589ee4ae39 100644
--- a/storage/innobase/dict/dict0stats.cc
+++ b/storage/innobase/dict/dict0stats.cc
@@ -720,71 +720,6 @@ dict_stats_copy(
 	dst->stat_initialized = TRUE;
 }
 
-/** Duplicate the stats of a table and its indexes.
-This function creates a dummy dict_table_t object and copies the input
-table's stats into it. The returned table object is not in the dictionary
-cache and cannot be accessed by any other threads. In addition to the
-members copied in dict_stats_table_clone_create() this function initializes
-the following:
-dict_table_t::stat_initialized
-dict_table_t::stat_persistent
-dict_table_t::stat_n_rows
-dict_table_t::stat_clustered_index_size
-dict_table_t::stat_sum_of_other_index_sizes
-dict_table_t::stat_modified_counter
-dict_index_t::stat_n_diff_key_vals[]
-dict_index_t::stat_n_sample_sizes[]
-dict_index_t::stat_n_non_null_key_vals[]
-dict_index_t::stat_index_size
-dict_index_t::stat_n_leaf_pages
-dict_index_t::stat_defrag_modified_counter
-dict_index_t::stat_defrag_n_pages_freed
-dict_index_t::stat_defrag_n_page_split
-The returned object should be freed with dict_stats_snapshot_free()
-when no longer needed.
-@param[in]	table	table whose stats to copy
-@return incomplete table object */
-static
-dict_table_t*
-dict_stats_snapshot_create(
-	dict_table_t*	table)
-{
-	mutex_enter(&dict_sys->mutex);
-
-	dict_table_stats_lock(table, RW_S_LATCH);
-
-	dict_stats_assert_initialized(table);
-
-	dict_table_t*	t;
-
-	t = dict_stats_table_clone_create(table);
-
-	dict_stats_copy(t, table, false);
-
-	t->stat_persistent = table->stat_persistent;
-	t->stats_auto_recalc = table->stats_auto_recalc;
-	t->stats_sample_pages = table->stats_sample_pages;
-	t->stats_bg_flag = table->stats_bg_flag;
-
-	dict_table_stats_unlock(table, RW_S_LATCH);
-
-	mutex_exit(&dict_sys->mutex);
-
-	return(t);
-}
-
-/*********************************************************************//**
-Free the resources occupied by an object returned by
-dict_stats_snapshot_create(). */
-static
-void
-dict_stats_snapshot_free(
-/*=====================*/
-	dict_table_t*	t)	/*!< in: dummy table object to free */
-{
-	dict_stats_table_clone_free(t);
-}
-
 /*********************************************************************//**
 Calculates new estimates for index statistics. This function is
 relatively quick and is used to calculate transient statistics that
@@ -2166,14 +2101,23 @@ will be saved on disk.
 @return DB_SUCCESS or error code */
 static
 dberr_t
-dict_stats_update_persistent(
-/*=========================*/
-	dict_table_t*	table)		/*!< in/out: table */
+dict_stats_update_persistent(dict_table_t* table, trx_t* trx)
 {
 	dict_index_t*	index;
 
 	DEBUG_PRINTF("%s(table=%s)\n", __func__, table->name);
 
+	/* We must lock the table already here in order to prevent
+	a locking conflict with RENAME TABLE (also as part of
+	ALTER TABLE...ALGORITHM=COPY). */
+	trx_start_if_not_started(trx, true);
+	dberr_t err = row_mysql_lock_table(trx, table, LOCK_IS,
+					   "updating persistent statistics");
+
+	if (err != DB_SUCCESS) {
+		return err;
+	}
+
 	dict_table_stats_lock(table, RW_X_LATCH);
 
 	/* analyze the clustered index first */
@@ -2375,7 +2319,7 @@ dict_stats_report_error(dict_table_t* table, bool defragment)
 }
 
 /** Save the table's statistics into the persistent statistics storage.
-@param[in]	table_orig	table whose stats to save
+@param[in]	table		table whose stats to save
 @param[in,out]	trx		transaction
 @param[in]	only_for_index	if this is non-NULL, then stats for indexes
 that are not equal to it will not be saved, if NULL, then all indexes' stats
@@ -2384,30 +2328,31 @@ are saved
 static
 dberr_t
 dict_stats_save(
-	dict_table_t*		table_orig,
+	dict_table_t*		table,
 	trx_t*			trx,
 	const index_id_t*	only_for_index = NULL)
 {
 	pars_info_t*	pinfo;
 	ib_time_t	now;
 	dberr_t		ret;
-	dict_table_t*	table;
 	char		db_utf8[MAX_DB_UTF8_LEN];
 	char		table_utf8[MAX_TABLE_UTF8_LEN];
 
 	ut_ad(trx->persistent_stats || trx->in_mysql_trx_list);
 
-	if (table_orig->is_readable()) {
-	} else {
-		return (dict_stats_report_error(table_orig));
+	if (!table->is_readable()) {
+		return dict_stats_report_error(table);
 	}
 
-	table = dict_stats_snapshot_create(table_orig);
+	trx_start_if_not_started(trx, true);
+	ret = row_mysql_lock_table(trx, table, LOCK_IS, "saving statistics");
+	if (ret != DB_SUCCESS) {
+		return ret;
+	}
 
 	dict_fs2utf8(table->name.m_name, db_utf8, sizeof(db_utf8),
 		     table_utf8, sizeof(table_utf8));
 
-
 	now = ut_time();
 
 	pinfo = pars_info_create();
@@ -2546,8 +2491,6 @@ dict_stats_save(
 			    << table->name << ": " << ut_strerr(ret);
 	}
 
-	dict_stats_snapshot_free(table);
-
 	return(ret);
 }
 
@@ -3136,7 +3079,7 @@ dict_stats_update(
 
 			dberr_t	err;
 
-			err = dict_stats_update_persistent(table);
+			err = dict_stats_update_persistent(table, trx);
 
 			if (err != DB_SUCCESS) {
 				return(err);
@@ -3310,16 +3253,12 @@ dict_stats_update(
 /** Remove the persistent statistics for an index.
 @param[in]	db_and_table	schema and table name, e.g., 'db/table'
 @param[in]	iname		index name
-@param[out]	errstr		error message (when not returning DB_SUCCESS)
-@param[in]	errstr_sz	sizeof errstr
 @param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 dberr_t
 dict_stats_drop_index(
 	const char*	db_and_table,
 	const char*	iname,
-	char*		errstr,
-	size_t		errstr_sz,
 	trx_t*		trx)
 {
 	char		db_utf8[MAX_DB_UTF8_LEN];
@@ -3327,8 +3266,6 @@ dict_stats_drop_index(
 	pars_info_t*	pinfo;
 	dberr_t		ret;
 
-	ut_ad(!mutex_own(&dict_sys->mutex));
-
 	/* skip indexes whose table names do not contain a database name
 	e.g. if we are dropping an index from SYS_TABLES */
 	if (strchr(db_and_table, '/') == NULL) {
@@ -3336,7 +3273,7 @@ dict_stats_drop_index(
 		return(DB_SUCCESS);
 	}
 
-	if (!dict_stats_persistent_storage_check(false)) {
+	if (!dict_stats_persistent_storage_check(true)) {
 		/* Statistics tables do not exist. */
 		return(DB_SUCCESS);
 	}
@@ -3352,8 +3289,6 @@ dict_stats_drop_index(
 
 	pars_info_add_str_literal(pinfo, "index_name", iname);
 
-	mutex_enter(&dict_sys->mutex);
-
 	ret = dict_stats_exec_sql(
 		pinfo,
 		"PROCEDURE DROP_INDEX_STATS () IS\n"
@@ -3364,36 +3299,8 @@ dict_stats_drop_index(
 		"index_name = :index_name;\n"
 		"END;\n", trx);
 
-	mutex_exit(&dict_sys->mutex);
-
-	switch (ret) {
-	case DB_STATS_DO_NOT_EXIST:
-	case DB_SUCCESS:
-		return(DB_SUCCESS);
-	case DB_QUE_THR_SUSPENDED:
-		ret = DB_LOCK_WAIT;
-		/* fall through */
-	default:
-		snprintf(errstr, errstr_sz,
-			 "Unable to delete statistics for index %s"
-			 " from %s%s: %s. They can be deleted later using"
-			 " DELETE FROM %s WHERE"
-			 " database_name = '%s' AND"
-			 " table_name = '%s' AND"
-			 " index_name = '%s';",
-			 iname,
-			 INDEX_STATS_NAME_PRINT,
-			 (ret == DB_LOCK_WAIT_TIMEOUT
-			  ? " because the rows are locked"
-			  : ""),
-			 ut_strerr(ret),
-			 INDEX_STATS_NAME_PRINT,
-			 db_utf8,
-			 table_utf8,
-			 iname);
-
-		ut_print_timestamp(stderr);
-		fprintf(stderr, " InnoDB: %s\n", errstr);
+	if (ret == DB_STATS_DO_NOT_EXIST) {
+		ret = DB_SUCCESS;
 	}
 
 	return(ret);
@@ -3449,15 +3356,11 @@ dict_stats_delete_from_index_stats(const char* db, const char* t, trx_t* trx)
 
 /** Remove the persistent statistics for a table and all of its indexes.
 @param[in]	db_and_table	schema and table name, e.g., 'db/table'
-@param[out]	errstr		error message (when not returning DB_SUCCESS)
-@param[in]	errstr_sz	sizeof errstr
 @param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 dberr_t
 dict_stats_drop_table(
 	const char*	db_and_table,
-	char*		errstr,
-	size_t		errstr_sz,
 	trx_t*		trx)
 {
 	char		db_utf8[MAX_DB_UTF8_LEN];
@@ -3488,39 +3391,13 @@ dict_stats_drop_table(
 
 	ret = dict_stats_delete_from_table_stats(db_utf8, table_utf8, trx);
 
-	if (ret == DB_SUCCESS) {
+	if (ret == DB_SUCCESS || ret == DB_STATS_DO_NOT_EXIST) {
 		ret = dict_stats_delete_from_index_stats(
 			db_utf8, table_utf8, trx);
 	}
 
-	switch (ret) {
-	case DB_SUCCESS:
-	case DB_STATS_DO_NOT_EXIST:
-		return(DB_SUCCESS);
-	case DB_QUE_THR_SUSPENDED:
-		ret = DB_LOCK_WAIT;
-		/* fall through */
-	default:
-		snprintf(errstr, errstr_sz,
-			 "Unable to delete statistics for table %s.%s: %s. "
-			 "They can be deleted later using "
-
-			 " DELETE FROM %s WHERE"
-			 " database_name = '%s' AND"
-			 " table_name = '%s';"
-
-			 " DELETE FROM %s WHERE"
-			 " database_name = '%s' AND"
-			 " table_name = '%s';",
-
-			 db_utf8, table_utf8,
-			 ut_strerr(ret),
-
-			 INDEX_STATS_NAME_PRINT,
-			 db_utf8, table_utf8,
-
-			 TABLE_STATS_NAME_PRINT,
-			 db_utf8, table_utf8);
+	if (ret == DB_STATS_DO_NOT_EXIST) {
+		ret = DB_SUCCESS;
 	}
 
 	return(ret);
@@ -3601,16 +3478,12 @@ dict_stats_rename_in_index_stats(
 /** Rename a table in the InnoDB persistent statistics storage.
 @param[in]	old_name	old schema and table name, e.g., 'db/table'
 @param[in]	new_name	new schema and table name, e.g., 'db/table'
-@param[out]	errstr		error message (when not returning DB_SUCCESS)
-@param[in]	errstr_sz	sizeof errstr
 @param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 dberr_t
 dict_stats_rename_table(
 	const char*	old_name,
 	const char*	new_name,
-	char*		errstr,
-	size_t		errstr_sz,
 	trx_t*		trx)
 {
 	char		old_db_utf8[MAX_DB_UTF8_LEN];
@@ -3628,7 +3501,7 @@ dict_stats_rename_table(
 		return(DB_SUCCESS);
 	}
 
-	if (!dict_stats_persistent_storage_check(false)) {
+	if (!dict_stats_persistent_storage_check(true)) {
 		/* Statistics tables do not exist. */
 		return(DB_SUCCESS);
 	}
@@ -3639,104 +3512,54 @@ dict_stats_rename_table(
 	dict_fs2utf8(new_name, new_db_utf8, sizeof(new_db_utf8),
 		     new_table_utf8, sizeof(new_table_utf8));
 
-	ulint	n_attempts = 0;
-	do {
+	for (bool retried = false;;) {
 		trx_savept_t savept = trx_savept_take(trx);
 
-		mutex_enter(&dict_sys->mutex);
-
 		ret = dict_stats_rename_in_table_stats(
 			old_db_utf8, old_table_utf8,
 			new_db_utf8, new_table_utf8, trx);
 
-		mutex_exit(&dict_sys->mutex);
+		if (ret != DB_DUPLICATE_KEY || retried) {
+			if (ret == DB_STATS_DO_NOT_EXIST) {
+				ret = DB_SUCCESS;
+			}
 
-		switch (ret) {
-		case DB_DUPLICATE_KEY:
-			trx_rollback_to_savepoint(trx, &savept);
-			mutex_enter(&dict_sys->mutex);
-			dict_stats_delete_from_table_stats(
-				new_db_utf8, new_table_utf8, trx);
-			mutex_exit(&dict_sys->mutex);
-			/* fall through */
-		case DB_LOCK_WAIT_TIMEOUT:
-			trx->error_state = DB_SUCCESS;
-			os_thread_sleep(200000 /* 0.2 sec */);
-			continue;
-		case DB_STATS_DO_NOT_EXIST:
-			ret = DB_SUCCESS;
-			break;
-		default:
 			break;
 		}
 
-		break;
-	} while (++n_attempts < 5);
+		retried = true;
+		trx->error_state = DB_SUCCESS;
+		trx_rollback_to_savepoint(trx, &savept);
 
-	const char* table_name = TABLE_STATS_NAME_PRINT;
+		dict_stats_delete_from_table_stats(
+			new_db_utf8, new_table_utf8, trx);
+	}
 
 	if (ret != DB_SUCCESS) {
-		goto err_exit;
+		return ret;
 	}
 
-	table_name = INDEX_STATS_NAME_PRINT;
-
-	n_attempts = 0;
-	do {
+	for (bool retried = false;;) {
 		trx_savept_t savept = trx_savept_take(trx);
 
-		mutex_enter(&dict_sys->mutex);
-
 		ret = dict_stats_rename_in_index_stats(
 			old_db_utf8, old_table_utf8,
 			new_db_utf8, new_table_utf8, trx);
 
-		mutex_exit(&dict_sys->mutex);
+		if (ret != DB_DUPLICATE_KEY || retried) {
+			if (ret == DB_STATS_DO_NOT_EXIST) {
+				ret = DB_SUCCESS;
+			}
 
-		switch (ret) {
-		case DB_DUPLICATE_KEY:
-			trx_rollback_to_savepoint(trx, &savept);
-			mutex_enter(&dict_sys->mutex);
-			dict_stats_delete_from_index_stats(
-				new_db_utf8, new_table_utf8, trx);
-			mutex_exit(&dict_sys->mutex);
-			/* fall through */
-		case DB_LOCK_WAIT_TIMEOUT:
-			trx->error_state = DB_SUCCESS;
-			os_thread_sleep(200000 /* 0.2 sec */);
-			continue;
-		case DB_STATS_DO_NOT_EXIST:
-			ret = DB_SUCCESS;
-			break;
-		default:
 			break;
 		}
 
-		break;
-	} while (++n_attempts < 5);
+		retried = true;
+		trx->error_state = DB_SUCCESS;
+		trx_rollback_to_savepoint(trx, &savept);
 
-	if (ret != DB_SUCCESS) {
-err_exit:
-		snprintf(errstr, errstr_sz,
-			 "Unable to rename statistics from"
-			 " %s.%s to %s.%s in %s: %s."
-			 " They can be renamed later using"
-
-			 " UPDATE %s SET"
-			 " database_name = '%s',"
-			 " table_name = '%s'"
-			 " WHERE"
-			 " database_name = '%s' AND"
-			 " table_name = '%s';",
-
-			 old_db_utf8, old_table_utf8,
-			 new_db_utf8, new_table_utf8,
-			 table_name,
-			 ut_strerr(ret),
-
-			 table_name,
-			 new_db_utf8, new_table_utf8,
-			 old_db_utf8, old_table_utf8);
+		dict_stats_delete_from_index_stats(
+			new_db_utf8, new_table_utf8, trx);
 	}
 
 	return(ret);
diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc
index 9cc4da545f6..dd397bb7b20 100644
--- a/storage/innobase/handler/ha_innodb.cc
+++ b/storage/innobase/handler/ha_innodb.cc
@@ -2119,9 +2119,6 @@ convert_error_code_to_mysql(
                          "InnoDB");
 		return(HA_ERR_INTERNAL_ERROR);
 
-	case DB_TABLE_IN_FK_CHECK:
-		return(HA_ERR_TABLE_IN_FK_CHECK);
-
 	case DB_TABLE_IS_BEING_USED:
 		return(HA_ERR_WRONG_COMMAND);
 
@@ -13903,18 +13900,9 @@ innobase_rename_table(
 	TrxInInnoDB	trx_in_innodb(trx);
 
 	trx_start_if_not_started(trx, true);
+	ut_ad(trx->will_lock > 0);
 
-	/* Serialize data dictionary operations with dictionary mutex:
-	no deadlocks can occur then in these operations. */
-
-	row_mysql_lock_data_dictionary(trx);
-
-	/* Transaction must be flagged as a locking transaction or it hasn't
-	been started yet. */
-
-	ut_a(trx->will_lock > 0);
-
-	error = row_rename_table_for_mysql(norm_from, norm_to, trx, TRUE);
+	error = row_rename_table_for_mysql(norm_from, norm_to, trx, true);
 
 	if (error != DB_SUCCESS) {
 		if (error == DB_TABLE_NOT_FOUND
@@ -13939,7 +13927,7 @@ innobase_rename_table(
 #endif /* _WIN32 */
 				trx_start_if_not_started(trx, true);
 				error = row_rename_table_for_mysql(
-					par_case_name, norm_to, trx, TRUE);
+					par_case_name, norm_to, trx, true);
 			}
 		}
 
@@ -13963,8 +13951,6 @@ innobase_rename_table(
 		}
 	}
 
-	row_mysql_unlock_data_dictionary(trx);
-
 	/* Flush the log to reduce probability that the .frm
 	files and the InnoDB data dictionary get out-of-sync
 	if the user runs with innodb_flush_log_at_trx_commit = 0 */
@@ -14011,32 +13997,6 @@ ha_innobase::rename_table(
 	DEBUG_SYNC(thd, "after_innobase_rename_table");
 
 	innobase_commit_low(trx);
-
-	if (error == DB_SUCCESS) {
-		char	norm_from[MAX_FULL_NAME_LEN];
-		char	norm_to[MAX_FULL_NAME_LEN];
-		char	errstr[512];
-		dberr_t	ret;
-
-		normalize_table_name(norm_from, from);
-		normalize_table_name(norm_to, to);
-
-		trx->error_state = DB_SUCCESS;
-		++trx->will_lock;
-		ret = dict_stats_rename_table(norm_from, norm_to,
-					      errstr, sizeof errstr, trx);
-
-		if (ret != DB_SUCCESS) {
-			trx_rollback_to_savepoint(trx, NULL);
-			ib::error() << errstr;
-
-			push_warning(thd, Sql_condition::WARN_LEVEL_WARN,
-				     ER_LOCK_WAIT_TIMEOUT, errstr);
-		} else {
-			innobase_commit_low(trx);
-		}
-	}
-
 	trx_free_for_mysql(trx);
 
 	/* Add a special case to handle the Duplicated Key error
diff --git a/storage/innobase/handler/handler0alter.cc b/storage/innobase/handler/handler0alter.cc
index 44c40d05b4c..ad75a5e8846 100644
--- a/storage/innobase/handler/handler0alter.cc
+++ b/storage/innobase/handler/handler0alter.cc
@@ -1,7 +1,7 @@
 /*****************************************************************************
 
 Copyright (c) 2005, 2017, Oracle and/or its affiliates. All Rights Reserved.
-Copyright (c) 2013, 2017, MariaDB Corporation.
+Copyright (c) 2013, 2018, MariaDB Corporation.
 
 This program is free software; you can redistribute it and/or modify it under
 the terms of the GNU General Public License as published by the Free Software
@@ -8210,6 +8210,63 @@ commit_cache_norebuild(
 	DBUG_RETURN(found);
 }
 
+/** Drop persistent statistics for the table in case the
+table is rebuilt, any indexes are dropped, or any
+virtual columns are added or dropped. */
+static
+void
+drop_stats(inplace_alter_handler_ctx** ctx_array, const char* name, trx_t* trx)
+{
+	bool warn = false;
+
+	for (inplace_alter_handler_ctx** pctx = ctx_array; *pctx; pctx++) {
+		ha_innobase_inplace_ctx*	ctx
+			= static_cast<ha_innobase_inplace_ctx*>
+			(*pctx);
+
+		if (ctx->need_rebuild()
+		    || ctx->num_to_drop_vcol || ctx->num_to_add_vcol) {
+			if (dict_stats_drop_table(ctx->old_table->name.m_name,
+						  trx)
+			    != DB_SUCCESS) {
+				warn = true;
+			}
+
+			continue;
+		}
+
+		for (ulint i = 0; i < ctx->num_to_drop_index; i++) {
+			dict_index_t*	index = ctx->drop_index[i];
+			DBUG_ASSERT(index->is_committed());
+			DBUG_ASSERT(index->table == ctx->new_table);
+			DBUG_ASSERT(index->to_be_dropped);
+
+			if (index->type & DICT_FTS) {
+				/* There are no index cardinality
+				statistics for FULLTEXT indexes. */
+				continue;
+			}
+
+			if (dict_stats_drop_index(ctx->new_table->name.m_name,
+						  index->name(), trx)
+			    != DB_SUCCESS) {
+				warn = true;
+			}
+		}
+	}
+
+	if (warn) {
+		ib::warn() << "ALTER TABLE failed to adjust statistics"
+			" for table '" << name << "'. Run ANALYZE TABLE.";
+		push_warning_printf(trx->mysql_thd,
+				    Sql_condition::WARN_LEVEL_WARN,
+				    ER_ALTER_INFO,
+				    "ALTER TABLE failed to adjust statistics"
+				    " for table '%s'." " Run ANALYZE TABLE.",
+				    name);
+	}
+}
+
 /** Adjust the persistent statistics after non-rebuilding ALTER TABLE.
 Remove statistics for dropped indexes, add statistics for created indexes
 and rename statistics for renamed indexes.
@@ -8236,44 +8293,6 @@ alter_stats_norebuild(
 		DBUG_RETURN(err);
 	}
 
-	/* Delete corresponding rows from the stats table. We do this
-	in a separate transaction from trx, because lock waits are not
-	allowed in a data dictionary transaction. (Lock waits are possible
-	on the statistics table, because it is directly accessible by users,
-	not covered by the dict_operation_lock.)
-
-	Because the data dictionary changes were already committed, orphaned
-	rows may be left in the statistics table if the system crashes.
-
-	FIXME: each change to the statistics tables is being committed in a
-	separate transaction, meaning that the operation is not atomic
-
-	FIXME: This will not drop the (unused) statistics for
-	FTS_DOC_ID_INDEX if it was a hidden index, dropped together
-	with the last renamining FULLTEXT index. */
-	for (i = 0; i < ha_alter_info->index_drop_count; i++) {
-		const KEY* key = ha_alter_info->index_drop_buffer[i];
-
-		if (key->flags & HA_FULLTEXT) {
-			/* There are no index cardinality
-			statistics for FULLTEXT indexes. */
-			continue;
-		}
-
-		char	errstr[1024];
-
-		dberr_t err2 = dict_stats_drop_index(
-			ctx->new_table->name.m_name, key->name,
-			errstr, sizeof errstr, trx);
-
-		if (err2 != DB_SUCCESS) {
-			err = err2;
-			push_warning(trx->mysql_thd,
-				     Sql_condition::WARN_LEVEL_WARN,
-				     ER_LOCK_WAIT_TIMEOUT, errstr);
-		}
-	}
-
 #ifdef MYSQL_RENAME_INDEX
 	for (i = 0; i < ha_alter_info->index_rename_count; i++) {
 		KEY_PAIR*	pair = &ha_alter_info->index_rename_buffer[i];
@@ -8315,74 +8334,6 @@ alter_stats_norebuild(
 	DBUG_RETURN(err);
 }
 
-/** Adjust the persistent statistics after rebuilding ALTER TABLE.
-Remove statistics for dropped indexes, add statistics for created indexes
-and rename statistics for renamed indexes.
-@param table		InnoDB table that was rebuilt by ALTER TABLE
-@param table_name	Table name in MySQL
-@param trx		transaction
-@return error code */
-static
-dberr_t
-alter_stats_rebuild(
-	dict_table_t*	table,
-	const char*	table_name,
-	trx_t*		trx)
-{
-	DBUG_ENTER("alter_stats_rebuild");
-
-	if (dict_table_is_discarded(table)
-	    || !dict_stats_is_persistent_enabled(table)) {
-		DBUG_RETURN(DB_SUCCESS);
-	}
-
-#ifndef DBUG_OFF
-	bool	file_unreadable_orig = false;
-#endif /* DBUG_OFF */
-
-	DBUG_EXECUTE_IF(
-		"ib_rename_index_fail2",
-		file_unreadable_orig = table->file_unreadable;
-		table->file_unreadable = true;
-	);
-
-	char	errstr[1024];
-	mutex_enter(&dict_sys->mutex);
-	dberr_t	ret = dict_stats_drop_table(table->name.m_name,
-					    errstr, sizeof errstr, trx);
-	mutex_exit(&dict_sys->mutex);
-	if (ret != DB_SUCCESS) {
-		push_warning_printf(
-			trx->mysql_thd,
-			Sql_condition::WARN_LEVEL_WARN,
-			ER_ALTER_INFO,
-			"Deleting persistent statistics"
-			" for rebuilt table '%s' in"
-			" InnoDB failed: %s",
-			table_name, errstr);
-		DBUG_RETURN(ret);
-	}
-
-	ret = dict_stats_update(table, DICT_STATS_RECALC_PERSISTENT, trx);
-
-	DBUG_EXECUTE_IF(
-		"ib_rename_index_fail2",
-		table->file_unreadable = file_unreadable_orig;
-	);
-
-	if (ret != DB_SUCCESS) {
-		push_warning_printf(
-			trx->mysql_thd,
-			Sql_condition::WARN_LEVEL_WARN,
-			ER_ALTER_INFO,
-			"Error updating stats for table '%s'"
-			" after table rebuild: %s",
-			table_name, ut_strerr(ret));
-	}
-
-	DBUG_RETURN(ret);
-}
-
 #ifndef DBUG_OFF
 # define DBUG_INJECT_CRASH(prefix, count)			\
 do {								\
@@ -8687,38 +8638,11 @@ ha_innobase::commit_inplace_alter_table(
 	if (fail) {
 		trx_rollback_for_mysql(trx);
 	} else if (!new_clustered) {
-		if (ctx0->num_to_drop_vcol || ctx0->num_to_add_vcol) {
-			DBUG_ASSERT(ctx0->old_table->get_ref_count() == 1);
-			bool warned = false;
-
-			for (inplace_alter_handler_ctx** pctx = ctx_array;
-			     *pctx; pctx++) {
-				ha_innobase_inplace_ctx*	ctx
-					= static_cast<ha_innobase_inplace_ctx*>
-					(*pctx);
-
-				DBUG_ASSERT(!ctx->need_rebuild());
-				char	errstr[1024];
-				if (dict_stats_drop_table(
-					    ctx->old_table->name.m_name,
-					    errstr, sizeof errstr, trx)
-				    != DB_SUCCESS && !warned) {
-					warned = true;
-					push_warning_printf(
-						m_user_thd,
-						Sql_condition::WARN_LEVEL_WARN,
-						ER_ALTER_INFO,
-						"Deleting persistent "
-						"statistics for table '%s' in"
-						" InnoDB failed: %s",
-						table_share->table_name.str,
-						errstr);
-				}
-			}
-		}
-
+		drop_stats(ctx_array, table_share->table_name.str, trx);
 		trx_commit_for_mysql(trx);
 	} else {
+		drop_stats(ctx_array, table_share->table_name.str, trx);
+
 		mtr_t	mtr;
 		mtr_start(&mtr);
 
@@ -9082,22 +9006,7 @@ ha_innobase::commit_inplace_alter_table(
 	trx->mysql_thd = m_user_thd;
 	dberr_t stats_err = DB_SUCCESS;
 
-	if (new_clustered) {
-		for (inplace_alter_handler_ctx** pctx = ctx_array;
-		     *pctx; pctx++) {
-			ha_innobase_inplace_ctx*	ctx
-				= static_cast<ha_innobase_inplace_ctx*>
-				(*pctx);
-			DBUG_ASSERT(ctx->need_rebuild());
-			stats_err = alter_stats_rebuild(
-				ctx->new_table, table->s->table_name.str, trx);
-			if (stats_err != DB_SUCCESS) {
-				break;
-			}
-			DBUG_INJECT_CRASH("ib_commit_inplace_crash",
-					  crash_inject_count++);
-		}
-	} else {
+	if (!new_clustered) {
 		for (inplace_alter_handler_ctx** pctx = ctx_array;
 		     *pctx; pctx++) {
 			ha_innobase_inplace_ctx*	ctx
diff --git a/storage/innobase/include/db0err.h b/storage/innobase/include/db0err.h
index ef6f8b39abb..4c048f20531 100644
--- a/storage/innobase/include/db0err.h
+++ b/storage/innobase/include/db0err.h
@@ -1,7 +1,7 @@
 /*****************************************************************************
 
 Copyright (c) 1996, 2016, Oracle and/or its affiliates. All Rights Reserved.
-Copyright (c) 2015, 2017, MariaDB Corporation.
+Copyright (c) 2015, 2018, MariaDB Corporation.
 
 This program is free software; you can redistribute it and/or modify it under
 the terms of the GNU General Public License as published by the Free Software
@@ -121,8 +121,6 @@ enum dberr_t {
 	DB_READ_ONLY,			/*!< Update operation attempted in
 					a read-only transaction */
 	DB_FTS_INVALID_DOCID,		/* FTS Doc ID cannot be zero */
-	DB_TABLE_IN_FK_CHECK,		/* table is being used in foreign
-					key check */
 	DB_ONLINE_LOG_TOO_BIG,		/*!< Modification log grew too big
 					during online index creation */
 
diff --git a/storage/innobase/include/dict0stats.h b/storage/innobase/include/dict0stats.h
index 014d7ea0d7f..6f13a9a808e 100644
--- a/storage/innobase/include/dict0stats.h
+++ b/storage/innobase/include/dict0stats.h
@@ -130,29 +130,21 @@ dict_stats_update(
 /** Remove the persistent statistics for an index.
 @param[in]	db_and_table	schema and table name, e.g., 'db/table'
 @param[in]	iname		index name
-@param[out]	errstr		error message (when not returning DB_SUCCESS)
-@param[in]	errstr_sz	sizeof errstr
 @param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 dberr_t
 dict_stats_drop_index(
 	const char*	db_and_table,
 	const char*	iname,
-	char*		errstr,
-	size_t		errstr_sz,
 	trx_t*		trx);
 
 /** Remove the persistent statistics for a table and all of its indexes.
 @param[in]	db_and_table	schema and table name, e.g., 'db/table'
-@param[out]	errstr		error message (when not returning DB_SUCCESS)
-@param[in]	errstr_sz	sizeof errstr
 @param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 dberr_t
 dict_stats_drop_table(
 	const char*	db_and_table,
-	char*		errstr,
-	size_t		errstr_sz,
 	trx_t*		trx);
 
 /** Calculate index statistics.
@@ -167,16 +159,12 @@ dict_stats_update_for_index(dict_index_t* index, trx_t* trx)
 /** Rename a table in the InnoDB persistent statistics storage.
 @param[in]	old_name	old schema and table name, e.g., 'db/table'
 @param[in]	new_name	new schema and table name, e.g., 'db/table'
-@param[out]	errstr		error message (when not returning DB_SUCCESS)
-@param[in]	errstr_sz	sizeof errstr
 @param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 dberr_t
 dict_stats_rename_table(
 	const char*	old_name,
 	const char*	new_name,
-	char*		errstr,
-	size_t		errstr_sz,
 	trx_t*		trx);
 #ifdef MYSQL_RENAME_INDEX
 /*********************************************************************//**
diff --git a/storage/innobase/include/row0mysql.h b/storage/innobase/include/row0mysql.h
index ff288dffbf0..999f0438376 100644
--- a/storage/innobase/include/row0mysql.h
+++ b/storage/innobase/include/row0mysql.h
@@ -420,16 +420,18 @@ ulint
 row_get_background_drop_list_len_low(void);
 /*======================================*/
 
-/*********************************************************************//**
-Sets an exclusive lock on a table.
+/** Acquire a table lock.
+@param[in,out]	trx	transaction
+@param[in]	table	table
+@param[in]	mode	lock mode
+@param[in]	op_info	string for trx->op_info
 @return error code or DB_SUCCESS */
 dberr_t
 row_mysql_lock_table(
-/*=================*/
-	trx_t*		trx,		/*!< in/out: transaction */
-	dict_table_t*	table,		/*!< in: table to lock */
-	enum lock_mode	mode,		/*!< in: LOCK_X or LOCK_S */
-	const char*	op_info)	/*!< in: string for trx->op_info */
+	trx_t*		trx,
+	dict_table_t*	table,
+	enum lock_mode	mode,
+	const char*	op_info)
 	MY_ATTRIBUTE((nonnull, warn_unused_result));
 
 /*********************************************************************//**
diff --git a/storage/innobase/lock/lock0lock.cc b/storage/innobase/lock/lock0lock.cc
index 7a1afec820c..9463d3b9c22 100644
--- a/storage/innobase/lock/lock0lock.cc
+++ b/storage/innobase/lock/lock0lock.cc
@@ -1,7 +1,7 @@
 /*****************************************************************************
 
 Copyright (c) 1996, 2016, Oracle and/or its affiliates. All Rights Reserved.
-Copyright (c) 2014, 2017, MariaDB Corporation.
+Copyright (c) 2014, 2018, MariaDB Corporation.
 
 This program is free software; you can redistribute it and/or modify it under
 the terms of the GNU General Public License as published by the Free Software
@@ -4664,7 +4664,13 @@ lock_table_enqueue_waiting(
 		break;
 	case TRX_DICT_OP_TABLE:
 	case TRX_DICT_OP_INDEX:
-		if (trx->dict_operation_lock_mode != RW_X_LATCH) {
+		if (trx->dict_operation_lock_mode == 0
+		    && !trx->lock.trx_locks.count && mode == LOCK_X) {
+			/* RENAME TABLE will first lock the user table,
+			within the dictionary transaction, before
+			locking the data dictionary. */
+			break;
+		} else if (trx->dict_operation_lock_mode != RW_X_LATCH) {
 		} else if (!strcmp(table->name.m_name,
 				   "mysql/innodb_table_stats")
 			   || !strcmp(table->name.m_name,
diff --git a/storage/innobase/row/row0mysql.cc b/storage/innobase/row/row0mysql.cc
index 6727f125bd3..8aaff448f9e 100644
--- a/storage/innobase/row/row0mysql.cc
+++ b/storage/innobase/row/row0mysql.cc
@@ -29,12 +29,13 @@ Created 9/17/2000 Heikki Tuuri
 #include <debug_sync.h>
 #include <gstream.h>
 #include <spatial.h>
+#include <sql_const.h>
+#include <sql_error.h>
 
 #include "row0mysql.h"
 #include "btr0sea.h"
 #include "dict0boot.h"
 #include "dict0crea.h"
-#include <sql_const.h>
 #include "dict0dict.h"
 #include "dict0load.h"
 #include "dict0stats.h"
@@ -3242,16 +3243,18 @@ row_discard_tablespace_for_mysql(
 	return(row_discard_tablespace_end(trx, table, err));
 }
 
-/*********************************************************************//**
-Sets an exclusive lock on a table.
+/** Acquire a table lock.
+@param[in,out]	trx	transaction
+@param[in]	table	table
+@param[in]	mode	lock mode
+@param[in]	op_info	string for trx->op_info
 @return error code or DB_SUCCESS */
 dberr_t
 row_mysql_lock_table(
-/*=================*/
-	trx_t*		trx,		/*!< in/out: transaction */
-	dict_table_t*	table,		/*!< in: table to lock */
-	enum lock_mode	mode,		/*!< in: LOCK_X or LOCK_S */
-	const char*	op_info)	/*!< in: string for trx->op_info */
+	trx_t*		trx,
+	dict_table_t*	table,
+	enum lock_mode	mode,
+	const char*	op_info)
 {
 	mem_heap_t*	heap;
 	que_thr_t*	thr;
@@ -3259,7 +3262,7 @@ row_mysql_lock_table(
 	sel_node_t*	node;
 
 	ut_ad(trx);
-	ut_ad(mode == LOCK_X || mode == LOCK_S);
+	ut_ad(mode == LOCK_X || mode == LOCK_S || mode == LOCK_IS);
 
 	heap = mem_heap_create(512);
 
@@ -3898,10 +3901,10 @@ row_drop_table_for_mysql(
 		if (!table->no_rollback())
 #endif
 		{
-			char	errstr[1024];
-			if (dict_stats_drop_table(name, errstr, sizeof errstr,
-						  trx) != DB_SUCCESS) {
-				ib::warn() << errstr;
+			if (dict_stats_drop_table(table->name.m_name, trx)
+			    != DB_SUCCESS) {
+				ib::warn() << "DROP TABLE " << table->name
+					   << " failed to drop statistics";
 			}
 
 			err = row_drop_ancillary_fts_tables(table, trx);
@@ -4336,13 +4339,13 @@ row_rename_table_for_mysql(
 	ulint		n_constraints_to_drop	= 0;
 	ibool		old_is_tmp, new_is_tmp;
 	pars_info_t*	info			= NULL;
-	int		retry;
 	bool		aux_fts_rename		= false;
 	char*		is_part 		= NULL;
 
 	ut_a(old_name != NULL);
 	ut_a(new_name != NULL);
 	ut_ad(trx->state == TRX_STATE_ACTIVE);
+	ut_ad(!commit || trx->mysql_thd);
 
 	if (high_level_read_only) {
 		return(DB_READ_ONLY);
@@ -4353,7 +4356,7 @@ row_rename_table_for_mysql(
 			<< new_name << " of type InnoDB. MySQL system tables"
 			" must be of the MyISAM type!";
 
-		goto funct_exit;
+		return DB_ERROR;
 	}
 
 	trx->op_info = "renaming table";
@@ -4363,6 +4366,8 @@ row_rename_table_for_mysql(
 
 	dict_locked = trx->dict_operation_lock_mode == RW_X_LATCH;
 
+	ut_ad(!commit == dict_locked);
+
 	table = dict_table_open_on_name(old_name, dict_locked, FALSE,
 					DICT_ERR_IGNORE_NONE);
 
@@ -4416,22 +4421,38 @@ row_rename_table_for_mysql(
 	}
 
 	if (!table) {
-		err = DB_TABLE_NOT_FOUND;
-		goto funct_exit;
+		return DB_TABLE_NOT_FOUND;
 
 	} else if (!table->is_readable()
 		   && fil_space_get(table->space) == NULL
 		   && !dict_table_is_discarded(table)) {
 
+		ib::error() << "Table " << table->name
+			<< " does not have a valid .ibd file. "
+			<< TROUBLESHOOTING_MSG;
 		err = DB_TABLE_NOT_FOUND;
 
-		ib::error() << "Table " << old_name << " does not have an .ibd"
-			" file in the database directory. "
-			<< TROUBLESHOOTING_MSG;
+err_exit:
+		dict_table_close(table, dict_locked, FALSE);
+		return err;
 
-		goto funct_exit;
+	} else if (commit) {
+		err = row_mysql_lock_table(trx, table, LOCK_X,
+					   "locking for rename table");
+		if (err != DB_SUCCESS) {
+			goto err_exit;
+		}
+	}
 
-	} else if (new_is_tmp) {
+	ut_ad(!my_atomic_loadlint(&table->n_foreign_key_checks_running));
+	ut_ad(!(table->stats_bg_flag & BG_STAT_IN_PROGRESS));
+
+	if (commit) {
+		row_mysql_lock_data_dictionary(trx);
+		dict_locked = true;
+	}
+
+	if (new_is_tmp) {
 		/* MySQL is doing an ALTER TABLE command and it renames the
 		original table to a temporary table name. We want to preserve
 		the original foreign key constraint definitions despite the
@@ -4449,23 +4470,6 @@ row_rename_table_for_mysql(
 		}
 	}
 
-	/* Is a foreign key check running on this table? */
-	for (retry = 0; retry < 100
-	     && table->n_foreign_key_checks_running > 0; ++retry) {
-		row_mysql_unlock_data_dictionary(trx);
-		os_thread_yield();
-		row_mysql_lock_data_dictionary(trx);
-	}
-
-	if (table->n_foreign_key_checks_running > 0) {
-		ib::error() << "In ALTER TABLE "
-			<< ut_get_name(trx, old_name)
-			<< " a FOREIGN KEY check is running. Cannot rename"
-			" table.";
-		err = DB_TABLE_IN_FK_CHECK;
-		goto funct_exit;
-	}
-
 	/* We use the private SQL parser of Innobase to generate the query
 	graphs needed in updating the dictionary data from system tables. */
 
@@ -4806,7 +4810,29 @@ row_rename_table_for_mysql(
 	}
 
 	if (commit) {
+		if (err == DB_SUCCESS) {
+			bool is_alter = (new_is_tmp && !old_is_tmp);
+			dberr_t stats_err = is_alter
+				? dict_stats_drop_table(old_name, trx)
+				: dict_stats_rename_table(old_name, new_name,
+							  trx);
+			if (stats_err != DB_SUCCESS) {
+				const char* errmsg = is_alter
+					? "ALTER TABLE failed to"
+					" adjust statistics for "
+					: "RENAME TABLE failed to"
+					" adjust statistics for ";
+				ib::error() << errmsg << old_name;
+				push_warning_printf(
+					trx->mysql_thd,
+					Sql_condition::WARN_LEVEL_WARN,
+					ER_LOCK_WAIT_TIMEOUT,
+					"%s%s", errmsg, old_name);
+			}
+		}
+
 		trx_commit_for_mysql(trx);
+		row_mysql_unlock_data_dictionary(trx);
 	}
 
 	if (UNIV_LIKELY_NULL(heap)) {
diff --git a/storage/innobase/row/row0trunc.cc b/storage/innobase/row/row0trunc.cc
index 5d4ab80f71c..ce470544430 100644
--- a/storage/innobase/row/row0trunc.cc
+++ b/storage/innobase/row/row0trunc.cc
@@ -2103,12 +2103,10 @@ row_truncate_table_for_mysql(
 	dict_table_autoinc_unlock(table);
 
 	if (trx_is_started(trx)) {
-		char	errstr[1024];
-		if (dict_stats_drop_table(table->name.m_name, errstr,
-					  sizeof errstr, trx) != DB_SUCCESS) {
-			ib::warn() << "Deleting persistent "
-				"statistics for table " << table->name
-				   << " failed: " << errstr;
+		if (dict_stats_drop_table(table->name.m_name, trx)
+		    != DB_SUCCESS) {
+			ib::warn() << "TRUNCATE TABLE " << table->name
+				   << " failed to drop statistics";
 		}
 
 		trx_commit_for_mysql(trx);
diff --git a/storage/innobase/ut/ut0ut.cc b/storage/innobase/ut/ut0ut.cc
index 28e327a2a77..0acfb10ad90 100644
--- a/storage/innobase/ut/ut0ut.cc
+++ b/storage/innobase/ut/ut0ut.cc
@@ -1,7 +1,7 @@
 /*****************************************************************************
 
 Copyright (c) 1994, 2016, Oracle and/or its affiliates. All Rights Reserved.
-Copyright (c) 2017, MariaDB Corporation.
+Copyright (c) 2017, 2018, MariaDB Corporation.
 
 This program is free software; you can redistribute it and/or modify it under
 the terms of the GNU General Public License as published by the Free Software
@@ -657,8 +657,6 @@ ut_strerr(
 		return("End of index");
 	case DB_IO_ERROR:
 		return("I/O error");
-	case DB_TABLE_IN_FK_CHECK:
-		return("Table is being used in foreign key check");
 	case DB_NOT_FOUND:
 		return("not found");
 	case DB_ONLINE_LOG_TOO_BIG:
-- 
2.15.1

