Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions mysql-test/suite/rpl/r/rpl_multi_table_delete_fk_cascade.result
Original file line number Diff line number Diff line change
@@ -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
89 changes: 89 additions & 0 deletions mysql-test/suite/rpl/t/rpl_multi_table_delete_fk_cascade.test
Original file line number Diff line number Diff line change
@@ -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
13 changes: 9 additions & 4 deletions sql/join_optimizer/join_optimizer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down
49 changes: 48 additions & 1 deletion sql/sql_base.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions sql/sql_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 7 additions & 4 deletions sql/sql_delete.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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;
}
Expand Down
Loading