diff --git a/mysql-test/suite/rpl/r/rpl_multi_table_delete_fk_cascade.result b/mysql-test/suite/rpl/r/rpl_multi_table_delete_fk_cascade.result new file mode 100644 index 000000000000..0fd33bce8e96 --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_multi_table_delete_fk_cascade.result @@ -0,0 +1,53 @@ +include/rpl/init_source_replica.inc +Warnings: +Note #### Sending passwords in plain text without SSL/TLS is extremely insecure. +Note #### Storing MySQL user name or password information in the connection metadata repository is not secure and is therefore not recommended. Please consider using the USER and PASSWORD connection options for START REPLICA; see the 'START REPLICA Syntax' in the MySQL Manual for more information. +[connection master] +# +# ON DELETE CASCADE +# +CREATE TABLE t1 (id INT PRIMARY KEY) ENGINE=InnoDB; +CREATE TABLE t2 ( +id INT PRIMARY KEY, +parent_id INT, +FOREIGN KEY (parent_id) REFERENCES t1(id) ON DELETE CASCADE +) ENGINE=InnoDB; +INSERT INTO t1 VALUES (1), (2); +INSERT INTO t2 VALUES (1, 1), (2, 1), (3, 2); +DELETE p, c FROM t1 p LEFT JOIN t2 c ON c.parent_id = p.id WHERE p.id = 1; +SELECT * FROM t1 ORDER BY id; +id +2 +SELECT * FROM t2 ORDER BY id; +id parent_id +3 2 +include/rpl/sync_to_replica.inc +include/diff_tables.inc [master:test.t1, slave:test.t1] +include/diff_tables.inc [master:test.t2, slave:test.t2] +[connection master] +DROP TABLE t2, t1; +# +# ON DELETE SET NULL, which is not deferred and not affected +# +CREATE TABLE t1 (id INT PRIMARY KEY) ENGINE=InnoDB; +CREATE TABLE t2 ( +id INT PRIMARY KEY, +parent_id INT, +FOREIGN KEY (parent_id) REFERENCES t1(id) ON DELETE SET NULL +) ENGINE=InnoDB; +INSERT INTO t1 VALUES (1), (2); +INSERT INTO t2 VALUES (1, 1), (2, 1), (3, 2); +DELETE p, c FROM t1 p LEFT JOIN t2 c ON c.parent_id = p.id WHERE p.id = 1; +SELECT * FROM t1 ORDER BY id; +id +2 +SELECT * FROM t2 ORDER BY id; +id parent_id +2 NULL +3 2 +include/rpl/sync_to_replica.inc +include/diff_tables.inc [master:test.t1, slave:test.t1] +include/diff_tables.inc [master:test.t2, slave:test.t2] +[connection master] +DROP TABLE t2, t1; +include/rpl/deinit.inc diff --git a/mysql-test/suite/rpl/t/rpl_multi_table_delete_fk_cascade.test b/mysql-test/suite/rpl/t/rpl_multi_table_delete_fk_cascade.test new file mode 100644 index 000000000000..008a44c96ef5 --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_multi_table_delete_fk_cascade.test @@ -0,0 +1,89 @@ +# ==== Purpose ==== +# +# Check that a multi-table DELETE which names both a foreign key parent table +# and a child table with a cascading delete rule does not break row-based +# replication. +# +# ==== Implementation ==== +# +# 1. On the source, run a multi-table DELETE covering a parent table and its +# ON DELETE CASCADE child. +# 2. Synchronize the replica and compare both tables. Before this fix the +# applier stopped with ER_KEY_NOT_FOUND: the parent row was deleted while +# the join was still scanning, so the cascade removed the child rows on the +# replica before the logged child row events were applied. +# 3. Repeat with an ON DELETE SET NULL child, which replicates correctly and +# is covered here so the difference stays visible. +# +# ==== References ==== +# +# Bug#80821: Replication breaks if multi-table DELETE is used in conjunction +# with Foreign Key +# Bug#102586: Foreign Key ON DELETE CASCADE breaks with RBR and multiple-table +# DELETE +# +############################################################################### +--source include/have_binlog_format_row.inc +--source include/rpl/init_source_replica.inc + +--echo # +--echo # ON DELETE CASCADE +--echo # + +CREATE TABLE t1 (id INT PRIMARY KEY) ENGINE=InnoDB; +CREATE TABLE t2 ( + id INT PRIMARY KEY, + parent_id INT, + FOREIGN KEY (parent_id) REFERENCES t1(id) ON DELETE CASCADE +) ENGINE=InnoDB; + +INSERT INTO t1 VALUES (1), (2); +INSERT INTO t2 VALUES (1, 1), (2, 1), (3, 2); + +DELETE p, c FROM t1 p LEFT JOIN t2 c ON c.parent_id = p.id WHERE p.id = 1; + +SELECT * FROM t1 ORDER BY id; +SELECT * FROM t2 ORDER BY id; + +--source include/rpl/sync_to_replica.inc + +--let $diff_tables= master:test.t1, slave:test.t1 +--source include/diff_tables.inc +--let $diff_tables= master:test.t2, slave:test.t2 +--source include/diff_tables.inc + +--let $rpl_connection_name= master +--source include/connection.inc +DROP TABLE t2, t1; + +--echo # +--echo # ON DELETE SET NULL, which is not deferred and not affected +--echo # + +CREATE TABLE t1 (id INT PRIMARY KEY) ENGINE=InnoDB; +CREATE TABLE t2 ( + id INT PRIMARY KEY, + parent_id INT, + FOREIGN KEY (parent_id) REFERENCES t1(id) ON DELETE SET NULL +) ENGINE=InnoDB; + +INSERT INTO t1 VALUES (1), (2); +INSERT INTO t2 VALUES (1, 1), (2, 1), (3, 2); + +DELETE p, c FROM t1 p LEFT JOIN t2 c ON c.parent_id = p.id WHERE p.id = 1; + +SELECT * FROM t1 ORDER BY id; +SELECT * FROM t2 ORDER BY id; + +--source include/rpl/sync_to_replica.inc + +--let $diff_tables= master:test.t1, slave:test.t1 +--source include/diff_tables.inc +--let $diff_tables= master:test.t2, slave:test.t2 +--source include/diff_tables.inc + +--let $rpl_connection_name= master +--source include/connection.inc +DROP TABLE t2, t1; + +--source include/rpl/deinit.inc diff --git a/sql/join_optimizer/join_optimizer.cc b/sql/join_optimizer/join_optimizer.cc index 75afd2a9ca94..037d3ff2ee1c 100644 --- a/sql/join_optimizer/join_optimizer.cc +++ b/sql/join_optimizer/join_optimizer.cc @@ -4757,8 +4757,7 @@ bool CostingReceiver::evaluate_secondary_engine_optimizer_state_request() { m_subgraph_pair_limit = restart_parameters.subgraph_pair_limit; DBUG_EXECUTE_IF("verify_hyp_opt_sg_pair_requested", { if (TraceStarted(m_thd) && m_subgraph_pair_limit > 0) { - Trace(m_thd) << "Hypergraph non zero SG pairs requested" - << "\n"; + Trace(m_thd) << "Hypergraph non zero SG pairs requested" << "\n"; } }); return true; @@ -7322,6 +7321,13 @@ bool IsImmediateDeleteCandidate(const Table_ref *table_ref, return false; } + // Cannot delete from the table immediately if the delete cascades to another + // table in the query, as the cascade would remove rows that the query still + // reads and deletes itself. See Bug#80821 and Bug#102586. + if (delete_cascades_to_queried_table(table_ref, query_block->leaf_tables)) { + return false; + } + return true; } @@ -10031,8 +10037,7 @@ static AccessPath *FindBestQueryPlanInner(THD *thd, Query_block *query_block, DBUG_EXECUTE_IF("verify_hyp_opt_sg_pair_requested", { if (TraceStarted(thd) && root_path_quality_status.subgraph_pair_limit > 0) { - Trace(thd) << "Hypergraph non zero SG pairs reset requested" - << "\n"; + Trace(thd) << "Hypergraph non zero SG pairs reset requested" << "\n"; } }); return nullptr; diff --git a/sql/sql_base.cc b/sql/sql_base.cc index 2c28c089dd59..dd6a05a3b17a 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -2256,6 +2256,53 @@ Table_ref *unique_table(const Table_ref *table, Table_ref *table_list, return dup; } +/** + Test whether deleting a row from the subject table of a multi-table DELETE + can cascade to another table which the same statement reads. + + Deleting from such a table while the join is still scanning is unsafe for + row-based replication: the cascade removes the child rows on the source and + logs row events for them, while the statement also logs the row events for + the child rows it deletes itself. On the replica the cascade has already + removed those rows by the time the logged child events are applied, which + breaks the applier with ER_KEY_NOT_FOUND. Deferring the delete until the + join has finished avoids the overlap. + + Only ON DELETE CASCADE deletes child rows, so only that rule is considered. + ON DELETE SET NULL updates the child rows instead, which leaves them + findable for the logged events and replicates correctly. + + @param table table to be checked (must be updatable base table) + @param leaf_tables leaf tables of the query block to check against + + @retval true Deleting from @p table cascades to one of @p leaf_tables. + @retval false No cascading dependency within the query. +*/ + +bool delete_cascades_to_queried_table(const Table_ref *table, + const Table_ref *leaf_tables) { + assert(table->table != nullptr); + + const TABLE_SHARE *share = table->table->s; + for (const TABLE_SHARE_FOREIGN_KEY_PARENT_INFO *fk_p = + share->foreign_key_parent; + fk_p < share->foreign_key_parent + share->foreign_key_parents; ++fk_p) { + if (fk_p->delete_rule != dd::Foreign_key::RULE_CASCADE) continue; + + for (const Table_ref *tl = leaf_tables; tl != nullptr; tl = tl->next_leaf) { + if (tl->table == nullptr) continue; // View or derived table. + const TABLE_SHARE *child_share = tl->table->s; + if (my_strcasecmp(table_alias_charset, child_share->db.str, + fk_p->referencing_table_db.str) == 0 && + my_strcasecmp(table_alias_charset, child_share->table_name.str, + fk_p->referencing_table_name.str) == 0) + return true; + } + } + + return false; +} + /** Issue correct error message in case we found 2 duplicate tables which prevent some update operation @@ -3182,7 +3229,7 @@ bool open_table(THD *thd, Table_ref *table_list, Open_table_context *ot_ctx) { } else if (table_list->open_strategy == Table_ref::OPEN_STUB) return false; -retry_share : { +retry_share: { Table_cache *tc = table_cache_manager.get_cache(thd); tc->lock(); diff --git a/sql/sql_base.h b/sql/sql_base.h index 820dbe88a5d8..4540757f4477 100644 --- a/sql/sql_base.h +++ b/sql/sql_base.h @@ -302,6 +302,8 @@ void close_thread_table(THD *thd, TABLE **table_ptr); bool close_temporary_tables(THD *thd); Table_ref *unique_table(const Table_ref *table, Table_ref *table_list, bool check_alias); +bool delete_cascades_to_queried_table(const Table_ref *table, + const Table_ref *leaf_tables); void drop_temporary_table(THD *thd, Table_ref *table_list); void close_temporary_table(THD *thd, TABLE *table, bool free_share, bool delete_table); diff --git a/sql/sql_delete.cc b/sql/sql_delete.cc index 4b8a602cb5df..0e200e8f4bb6 100644 --- a/sql/sql_delete.cc +++ b/sql/sql_delete.cc @@ -631,8 +631,8 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) { break; } - DBUG_LOG("jdv_dml", "DML-DELETE: " - << " table_list->field_translation->name: " + DBUG_LOG("jdv_dml", + "DML-DELETE: " << " table_list->field_translation->name: " << table_list->field_translation->name << " ->type():" << table_list->field_translation->item->type()); @@ -1333,11 +1333,14 @@ table_map GetImmediateDeleteTables(const JOIN *join, table_map delete_tables) { tr = tr->next_leaf) { if (!tr->is_deleted()) continue; - if (unique_table(tr, join->tables_list, false) != nullptr) { + if (unique_table(tr, join->tables_list, false) != nullptr || + delete_cascades_to_queried_table(tr, join->query_block->leaf_tables)) { /* If the table being deleted from is also referenced in the query, defer delete so that the delete doesn't interfere with reading of this - table. + table. The same applies if deleting from the table cascades to another + table in the query, since the cascade would remove rows that the query + still reads and deletes itself. See Bug#80821 and Bug#102586. */ return 0; }