From 66cb600da99562a851eb642e1ce542f5aa73375e Mon Sep 17 00:00:00 2001 From: sjaakola Date: Thu, 2 Apr 2026 21:22:57 +0300 Subject: [PATCH 1/3] MDEV-38243 Write binlog row events for changes done by cascading FK operations This commit implements a feature which changes the handling of cascading foreign key operations to write the changes of cascading operations into binlog. The applying of such transaction, in the slave node, will apply just the binlog events, and does not execute the actual foreign key cascade operation. This will simplify the slave side replication applying and make it more predictable in terms of potential interference with other parallel applying happning in the node. This feature can be turned ON/OFF by new variable: rpl_use_binlog_events_for_fk_cascade, with default value OFF The actual implementation is largely by windsurf. The commit has also mtr tests for testing rpl_use_binlog_events_for_fk_cascade feature: rpl.rpl_fk_cascade_binlog_row, rpl.rpl_fk_set_null_binlog_row and rpl.fk_cascade_binlog_row_rollback --- include/mysql/plugin.h | 1 + include/mysql/service_thd_binlog.h | 47 +++ include/mysql/service_wsrep.h | 2 + mysql-test/main/mysqld--help.result | 4 + .../rpl/r/rpl_fk_cascade_binlog_row.result | 85 +++++ .../rpl_fk_cascade_binlog_row_ordering.result | 37 +++ .../rpl_fk_cascade_binlog_row_rollback.result | 140 ++++++++ .../rpl/r/rpl_fk_set_null_binlog_row.result | 61 ++++ .../rpl/t/rpl_fk_cascade_binlog_row.test | 133 ++++++++ .../t/rpl_fk_cascade_binlog_row_ordering.test | 69 ++++ .../t/rpl_fk_cascade_binlog_row_rollback.test | 201 ++++++++++++ .../rpl/t/rpl_fk_set_null_binlog_row.test | 105 ++++++ .../sys_vars/r/sysvars_server_embedded.result | 10 + .../r/sysvars_server_notembedded.result | 10 + sql/handler.cc | 73 ++++- sql/handler.h | 13 +- sql/log.cc | 25 +- sql/log_event.h | 2 + sql/log_event_server.cc | 8 +- sql/service_wsrep.cc | 5 + sql/sql_class.cc | 58 ++++ sql/sql_class.h | 6 + sql/sys_vars.cc | 20 ++ storage/innobase/handler/ha_innodb.cc | 86 ++++- storage/innobase/handler/ha_innodb.h | 18 ++ storage/innobase/include/row0sel.h | 12 +- storage/innobase/include/trx0trx.h | 15 + storage/innobase/row/row0ins.cc | 300 +++++++++++++++++- storage/innobase/row/row0sel.cc | 2 +- storage/innobase/trx/trx0trx.cc | 25 +- 30 files changed, 1556 insertions(+), 17 deletions(-) create mode 100644 include/mysql/service_thd_binlog.h create mode 100644 mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row.result create mode 100644 mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_ordering.result create mode 100644 mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_rollback.result create mode 100644 mysql-test/suite/rpl/r/rpl_fk_set_null_binlog_row.result create mode 100644 mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row.test create mode 100644 mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_ordering.test create mode 100644 mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_rollback.test create mode 100644 mysql-test/suite/rpl/t/rpl_fk_set_null_binlog_row.test diff --git a/include/mysql/plugin.h b/include/mysql/plugin.h index b4d1c1c6b54d8..acdd30183048e 100644 --- a/include/mysql/plugin.h +++ b/include/mysql/plugin.h @@ -697,6 +697,7 @@ int thd_in_lock_tables(const MYSQL_THD thd); int thd_tablespace_op(const MYSQL_THD thd); long long thd_test_options(const MYSQL_THD thd, long long test_options); int thd_sql_command(const MYSQL_THD thd); + struct DDL_options_st; struct DDL_options_st *thd_ddl_options(const MYSQL_THD thd); void thd_storage_lock_wait(MYSQL_THD thd, long long value); diff --git a/include/mysql/service_thd_binlog.h b/include/mysql/service_thd_binlog.h new file mode 100644 index 0000000000000..912f4213ee662 --- /dev/null +++ b/include/mysql/service_thd_binlog.h @@ -0,0 +1,47 @@ +/* Copyright (c) 2026, 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 Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +struct TABLE; +class Event_log; +class binlog_cache_data; + +int thd_is_current_stmt_binlog_format_row(const MYSQL_THD thd); + +int thd_rpl_use_binlog_events_for_fk_cascade(const MYSQL_THD thd); + +void thd_binlog_mark_fk_cascade_events(MYSQL_THD thd); + +int thd_binlog_update_row(MYSQL_THD thd, struct TABLE *table, + class Event_log *bin_log, + class binlog_cache_data *cache_data, + int is_trans, unsigned long row_image, + const unsigned char *before_record, + const unsigned char *after_record); + +int thd_binlog_delete_row(MYSQL_THD thd, struct TABLE *table, + class Event_log *bin_log, + class binlog_cache_data *cache_data, + int is_trans, unsigned long row_image, + const unsigned char *before_record); + +#ifdef __cplusplus +} +#endif diff --git a/include/mysql/service_wsrep.h b/include/mysql/service_wsrep.h index 8c001ca147063..9d0114ac04f0e 100644 --- a/include/mysql/service_wsrep.h +++ b/include/mysql/service_wsrep.h @@ -153,6 +153,7 @@ extern struct wsrep_service_st { #define wsrep_report_bf_lock_wait(T,I) wsrep_service->wsrep_report_bf_lock_wait(T,I) #define wsrep_thd_set_PA_unsafe(T) wsrep_service->wsrep_thd_set_PA_unsafe_func(T) #define wsrep_get_domain_id(T) wsrep_service->wsrep_get_domain_id_func(T) +#define wsrep_emulate_binlog(T) wsrep_service->wsrep_emulate_binlog_func(T) #else #define MYSQL_SERVICE_WSREP_STATIC_INCLUDED @@ -265,5 +266,6 @@ extern "C" void wsrep_report_bf_lock_wait(const THD *thd, /* declare parallel applying unsafety for the THD */ extern "C" void wsrep_thd_set_PA_unsafe(MYSQL_THD thd); extern "C" uint32 wsrep_get_domain_id(); +extern "C" my_bool wsrep_emulate_binlog(const MYSQL_THD thd); #endif #endif /* MYSQL_SERVICE_WSREP_INCLUDED */ diff --git a/mysql-test/main/mysqld--help.result b/mysql-test/main/mysqld--help.result index 9f06fe5f11dd6..c0d887e61b6eb 100644 --- a/mysql-test/main/mysqld--help.result +++ b/mysql-test/main/mysqld--help.result @@ -1390,6 +1390,9 @@ The following specify which files/extra groups are read (specified before remain play when stop slave is executed --rpl-semi-sync-slave-trace-level=# The tracing level for semi-sync replication + --rpl-use-binlog-events-for-fk-cascade + If enabled, the master will write row events for foreign + key cascade operations --safe-mode Skip some optimize stages (for testing). Deprecated, will be removed in a future release. --safe-user-create Don't allow new user creation by the user who has no @@ -2096,6 +2099,7 @@ rpl-semi-sync-slave-delay-master FALSE rpl-semi-sync-slave-enabled FALSE rpl-semi-sync-slave-kill-conn-timeout 5 rpl-semi-sync-slave-trace-level 32 +rpl-use-binlog-events-for-fk-cascade FALSE safe-user-create FALSE secure-file-priv (No default value) secure-timestamp NO diff --git a/mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row.result b/mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row.result new file mode 100644 index 0000000000000..5eb0ef025a8bf --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row.result @@ -0,0 +1,85 @@ +include/master-slave.inc +[connection master] +SET @old_binlog_format := @@session.binlog_format; +SET @old_default_storage_engine := @@session.default_storage_engine; +SET @old_rpl_use_binlog_events_for_fk_cascade := @@session.rpl_use_binlog_events_for_fk_cascade; +SET SESSION default_storage_engine=InnoDB; +SET SESSION binlog_format=ROW; +SET SESSION rpl_use_binlog_events_for_fk_cascade=0; +CREATE TABLE p ( +id INT PRIMARY KEY, +v INT +) ENGINE=InnoDB; +CREATE TABLE c ( +id INT PRIMARY KEY, +pid INT, +v INT, +KEY(pid), +CONSTRAINT fk FOREIGN KEY (pid) REFERENCES p(id) +ON DELETE CASCADE +ON UPDATE CASCADE +) ENGINE=InnoDB; +INSERT INTO p VALUES (1, 10); +INSERT INTO c VALUES (100, 1, 20); +UPDATE p SET id=2 WHERE id=1; +connection slave; +connection slave; +SELECT pid FROM c ORDER BY id; +pid +2 +connection master; +DELETE FROM p WHERE id=2; +connection slave; +connection slave; +SELECT COUNT(*) FROM p; +COUNT(*) +0 +SELECT COUNT(*) FROM c; +COUNT(*) +0 +connection master; +FLUSH BINARY LOGS; +NOT FOUND /### UPDATE `test`.`c`/ in fk_cascade_binlog_row_off.sql +NOT FOUND /### DELETE FROM `test`.`c`/ in fk_cascade_binlog_row_off.sql +DROP TABLE c, p; +SET SESSION rpl_use_binlog_events_for_fk_cascade=1; +CREATE TABLE p ( +id INT PRIMARY KEY, +v INT +) ENGINE=InnoDB; +CREATE TABLE c ( +id INT PRIMARY KEY, +pid INT, +v INT, +KEY(pid), +CONSTRAINT fk FOREIGN KEY (pid) REFERENCES p(id) +ON DELETE CASCADE +ON UPDATE CASCADE +) ENGINE=InnoDB; +INSERT INTO p VALUES (1, 10); +INSERT INTO c VALUES (100, 1, 20); +UPDATE p SET id=2 WHERE id=1; +connection slave; +connection slave; +SELECT pid FROM c ORDER BY id; +pid +2 +connection master; +DELETE FROM p WHERE id=2; +connection slave; +connection slave; +SELECT COUNT(*) FROM p; +COUNT(*) +0 +SELECT COUNT(*) FROM c; +COUNT(*) +0 +connection master; +FLUSH BINARY LOGS; +FOUND 1 /### UPDATE `test`.`c`/ in fk_cascade_binlog_row_on.sql +FOUND 1 /### DELETE FROM `test`.`c`/ in fk_cascade_binlog_row_on.sql +DROP TABLE c, p; +SET SESSION default_storage_engine=@old_default_storage_engine; +SET SESSION binlog_format=@old_binlog_format; +SET SESSION rpl_use_binlog_events_for_fk_cascade=@old_rpl_use_binlog_events_for_fk_cascade; +include/rpl_end.inc diff --git a/mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_ordering.result b/mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_ordering.result new file mode 100644 index 0000000000000..40000d8c75e2c --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_ordering.result @@ -0,0 +1,37 @@ +include/master-slave.inc +[connection master] +SET @old_binlog_format := @@session.binlog_format; +SET @old_rpl_use_binlog_events_for_fk_cascade := @@session.rpl_use_binlog_events_for_fk_cascade; +SET SESSION binlog_format=ROW; +SET SESSION rpl_use_binlog_events_for_fk_cascade=1; +CREATE TABLE p ( +id INT PRIMARY KEY +) ENGINE=InnoDB; +CREATE TABLE c ( +id INT PRIMARY KEY, +a INT, +b INT, +KEY(a), +KEY(b), +CONSTRAINT fk_a FOREIGN KEY (a) REFERENCES p(id) ON DELETE SET NULL, +CONSTRAINT fk_b FOREIGN KEY (b) REFERENCES p(id) ON DELETE CASCADE +) ENGINE=InnoDB; +INSERT INTO p VALUES (1); +INSERT INTO c VALUES (10, 1, 1); +connection slave; +connection master; +DELETE FROM p WHERE id=1; +# master: child row cascade-deleted +SELECT * FROM c; +id a b +connection slave; +connection slave; +# slave must match master (row deleted); a wrong DELETE-before-UPDATE +# ordering would have stopped the SQL thread on the UPDATE event +SELECT * FROM c; +id a b +connection master; +DROP TABLE c, p; +SET SESSION binlog_format=@old_binlog_format; +SET SESSION rpl_use_binlog_events_for_fk_cascade=@old_rpl_use_binlog_events_for_fk_cascade; +include/rpl_end.inc diff --git a/mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_rollback.result b/mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_rollback.result new file mode 100644 index 0000000000000..25eee590001fa --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_rollback.result @@ -0,0 +1,140 @@ +include/master-slave.inc +[connection master] +SET @old_binlog_format := @@session.binlog_format; +SET @old_default_storage_engine := @@session.default_storage_engine; +SET @old_rpl_use_binlog_events_for_fk_cascade := @@session.rpl_use_binlog_events_for_fk_cascade; +SET SESSION default_storage_engine=InnoDB; +SET SESSION binlog_format=ROW; +SET SESSION rpl_use_binlog_events_for_fk_cascade=1; +CREATE TABLE p ( +id INT PRIMARY KEY, +v INT +) ENGINE=InnoDB; +CREATE TABLE c ( +id INT PRIMARY KEY, +pid INT, +v INT, +KEY(pid), +CONSTRAINT fk FOREIGN KEY (pid) REFERENCES p(id) +ON DELETE CASCADE +ON UPDATE CASCADE +) ENGINE=InnoDB; +INSERT INTO p VALUES (1, 10); +INSERT INTO c VALUES (100, 1, 20); +connection slave; +connection master; +# +# Phase 1: full transaction ROLLBACK +# +FLUSH BINARY LOGS; +BEGIN; +UPDATE p SET id=2 WHERE id=1; +ROLLBACK; +# master: parent and child unchanged +SELECT * FROM p ORDER BY id; +id v +1 10 +SELECT * FROM c ORDER BY id; +id pid v +100 1 20 +connection slave; +connection slave; +# slave: child unchanged +SELECT * FROM c ORDER BY id; +id pid v +100 1 20 +connection master; +FLUSH BINARY LOGS; +# no cascade child row event must be present +NOT FOUND /### UPDATE `test`.`c`/ in fk_cascade_rollback_full.sql +# +# Phase 2: ROLLBACK TO SAVEPOINT (surviving work before the savepoint) +# +FLUSH BINARY LOGS; +BEGIN; +INSERT INTO p VALUES (3, 30); +SAVEPOINT sp1; +UPDATE p SET id=2 WHERE id=1; +ROLLBACK TO SAVEPOINT sp1; +COMMIT; +# master: cascade undone, INSERT before savepoint kept +SELECT * FROM p ORDER BY id; +id v +1 10 +3 30 +SELECT * FROM c ORDER BY id; +id pid v +100 1 20 +connection slave; +connection slave; +# slave: child unchanged, parent has surviving row +SELECT * FROM p ORDER BY id; +id v +1 10 +3 30 +SELECT * FROM c ORDER BY id; +id pid v +100 1 20 +connection master; +FLUSH BINARY LOGS; +# cascade child row event must be absent +NOT FOUND /### UPDATE `test`.`c`/ in fk_cascade_rollback_sp.sql +# surviving INSERT before the savepoint must be replicated +FOUND 1 /### INSERT INTO `test`.`p`/ in fk_cascade_rollback_sp.sql +DROP TABLE c, p; +# +# Phase 3: statement rollback, transaction continues and commits +# +CREATE TABLE p ( +id INT PRIMARY KEY, +u INT, +UNIQUE KEY uq_u(u) +) ENGINE=InnoDB; +CREATE TABLE c ( +cid INT PRIMARY KEY, +pid INT, +KEY(pid), +CONSTRAINT fk FOREIGN KEY (pid) REFERENCES p(id) +ON UPDATE CASCADE +) ENGINE=InnoDB; +INSERT INTO p VALUES (1, 100), (2, 200); +INSERT INTO c VALUES (11, 1); +connection slave; +connection master; +FLUSH BINARY LOGS; +BEGIN; +UPDATE p SET id=id+10, u=100 WHERE id IN (1,2) ORDER BY id; +ERROR 23000: Duplicate entry '100' for key 'uq_u' +INSERT INTO p VALUES (9, 900); +COMMIT; +# master: failed UPDATE rolled back, INSERT committed +SELECT * FROM p ORDER BY id; +id u +1 100 +2 200 +9 900 +SELECT * FROM c ORDER BY cid; +cid pid +11 1 +connection slave; +connection slave; +# slave: child unchanged, parent has committed row +SELECT * FROM p ORDER BY id; +id u +1 100 +2 200 +9 900 +SELECT * FROM c ORDER BY cid; +cid pid +11 1 +connection master; +FLUSH BINARY LOGS; +# discarded cascade child row event must be absent +NOT FOUND /### UPDATE `test`.`c`/ in fk_cascade_rollback_stmt.sql +# committed INSERT must be replicated +FOUND 1 /### INSERT INTO `test`.`p`/ in fk_cascade_rollback_stmt.sql +DROP TABLE c, p; +SET SESSION default_storage_engine=@old_default_storage_engine; +SET SESSION binlog_format=@old_binlog_format; +SET SESSION rpl_use_binlog_events_for_fk_cascade=@old_rpl_use_binlog_events_for_fk_cascade; +include/rpl_end.inc diff --git a/mysql-test/suite/rpl/r/rpl_fk_set_null_binlog_row.result b/mysql-test/suite/rpl/r/rpl_fk_set_null_binlog_row.result new file mode 100644 index 0000000000000..43a6c65f91b69 --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_fk_set_null_binlog_row.result @@ -0,0 +1,61 @@ +include/master-slave.inc +[connection master] +SET @old_binlog_format := @@session.binlog_format; +SET @old_default_storage_engine := @@session.default_storage_engine; +SET @old_rpl_use_binlog_events_for_fk_cascade := @@session.rpl_use_binlog_events_for_fk_cascade; +SET SESSION default_storage_engine=InnoDB; +SET SESSION binlog_format=ROW; +SET SESSION rpl_use_binlog_events_for_fk_cascade=0; +CREATE TABLE p ( +id INT PRIMARY KEY, +v INT +) ENGINE=InnoDB; +CREATE TABLE c ( +id INT PRIMARY KEY, +pid INT NULL, +v INT, +KEY(pid), +CONSTRAINT fk FOREIGN KEY (pid) REFERENCES p(id) +ON DELETE SET NULL +ON UPDATE SET NULL +) ENGINE=InnoDB; +INSERT INTO p VALUES (1, 10); +INSERT INTO c VALUES (100, 1, 20); +UPDATE p SET id=2 WHERE id=1; +connection slave; +SELECT pid FROM c ORDER BY id; +pid +NULL +connection master; +FLUSH BINARY LOGS; +NOT FOUND /### UPDATE `test`.`c`/ in fk_set_null_binlog_row_off.sql +DROP TABLE c, p; +SET SESSION rpl_use_binlog_events_for_fk_cascade=1; +CREATE TABLE p ( +id INT PRIMARY KEY, +v INT +) ENGINE=InnoDB; +CREATE TABLE c ( +id INT PRIMARY KEY, +pid INT NULL, +v INT, +KEY(pid), +CONSTRAINT fk FOREIGN KEY (pid) REFERENCES p(id) +ON DELETE SET NULL +ON UPDATE SET NULL +) ENGINE=InnoDB; +INSERT INTO p VALUES (1, 10); +INSERT INTO c VALUES (100, 1, 20); +UPDATE p SET id=2 WHERE id=1; +connection slave; +SELECT pid FROM c ORDER BY id; +pid +NULL +connection master; +FLUSH BINARY LOGS; +FOUND 1 /### UPDATE `test`.`c`/ in fk_set_null_binlog_row_on.sql +DROP TABLE c, p; +SET SESSION default_storage_engine=@old_default_storage_engine; +SET SESSION binlog_format=@old_binlog_format; +SET SESSION rpl_use_binlog_events_for_fk_cascade=@old_rpl_use_binlog_events_for_fk_cascade; +include/rpl_end.inc diff --git a/mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row.test b/mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row.test new file mode 100644 index 0000000000000..73d083a66dda5 --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row.test @@ -0,0 +1,133 @@ +--source include/have_innodb.inc +--source include/have_log_bin.inc +--source include/have_binlog_format_row.inc + +--source include/master-slave.inc + +SET @old_binlog_format := @@session.binlog_format; +SET @old_default_storage_engine := @@session.default_storage_engine; +SET @old_rpl_use_binlog_events_for_fk_cascade := @@session.rpl_use_binlog_events_for_fk_cascade; + +SET SESSION default_storage_engine=InnoDB; +SET SESSION binlog_format=ROW; + +# Phase A: feature OFF (no explicit FK cascade row events in binlog) +SET SESSION rpl_use_binlog_events_for_fk_cascade=0; + +CREATE TABLE p ( + id INT PRIMARY KEY, + v INT +) ENGINE=InnoDB; + +CREATE TABLE c ( + id INT PRIMARY KEY, + pid INT, + v INT, + KEY(pid), + CONSTRAINT fk FOREIGN KEY (pid) REFERENCES p(id) + ON DELETE CASCADE + ON UPDATE CASCADE +) ENGINE=InnoDB; + +INSERT INTO p VALUES (1, 10); +INSERT INTO c VALUES (100, 1, 20); + +UPDATE p SET id=2 WHERE id=1; + +sync_slave_with_master; +connection slave; +SELECT pid FROM c ORDER BY id; + +connection master; +DELETE FROM p WHERE id=2; + +sync_slave_with_master; +connection slave; +SELECT COUNT(*) FROM p; +SELECT COUNT(*) FROM c; + +connection master; + +--let $binlog = query_get_value(SHOW MASTER STATUS, File, 1) +--let $datadir = `SELECT @@datadir` + +FLUSH BINARY LOGS; + +--exec $MYSQL_BINLOG --verbose --verbose --base64-output=DECODE-ROWS $datadir/$binlog > $MYSQLTEST_VARDIR/tmp/fk_cascade_binlog_row_off.sql + +--let SEARCH_PATTERN= ### UPDATE `test`.`c` +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/fk_cascade_binlog_row_off.sql +--let SEARCH_ABORT= FOUND +--source include/search_pattern_in_file.inc + +--let SEARCH_PATTERN= ### DELETE FROM `test`.`c` +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/fk_cascade_binlog_row_off.sql +--let SEARCH_ABORT= FOUND +--source include/search_pattern_in_file.inc + +DROP TABLE c, p; + +# Phase B: feature ON (explicit FK cascade row events in binlog) +SET SESSION rpl_use_binlog_events_for_fk_cascade=1; + +CREATE TABLE p ( + id INT PRIMARY KEY, + v INT +) ENGINE=InnoDB; + +CREATE TABLE c ( + id INT PRIMARY KEY, + pid INT, + v INT, + KEY(pid), + CONSTRAINT fk FOREIGN KEY (pid) REFERENCES p(id) + ON DELETE CASCADE + ON UPDATE CASCADE +) ENGINE=InnoDB; + +INSERT INTO p VALUES (1, 10); +INSERT INTO c VALUES (100, 1, 20); + +UPDATE p SET id=2 WHERE id=1; + +sync_slave_with_master; +connection slave; +SELECT pid FROM c ORDER BY id; + +connection master; +DELETE FROM p WHERE id=2; + +sync_slave_with_master; +connection slave; +SELECT COUNT(*) FROM p; +SELECT COUNT(*) FROM c; + +connection master; + +--let $binlog = query_get_value(SHOW MASTER STATUS, File, 1) +--let $datadir = `SELECT @@datadir` + +FLUSH BINARY LOGS; + +--exec $MYSQL_BINLOG --verbose --verbose --base64-output=DECODE-ROWS $datadir/$binlog > $MYSQLTEST_VARDIR/tmp/fk_cascade_binlog_row_on.sql + +--let SEARCH_PATTERN= ### UPDATE `test`.`c` +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/fk_cascade_binlog_row_on.sql +--let SEARCH_ABORT= NOT FOUND|FOUND 0|FOUND [2-9] +--source include/search_pattern_in_file.inc + +--let SEARCH_PATTERN= ### DELETE FROM `test`.`c` +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/fk_cascade_binlog_row_on.sql +--let SEARCH_ABORT= NOT FOUND|FOUND 0|FOUND [2-9] +--source include/search_pattern_in_file.inc + +DROP TABLE c, p; + +SET SESSION default_storage_engine=@old_default_storage_engine; +SET SESSION binlog_format=@old_binlog_format; +SET SESSION rpl_use_binlog_events_for_fk_cascade=@old_rpl_use_binlog_events_for_fk_cascade; + +--remove_file $MYSQLTEST_VARDIR/tmp/fk_cascade_binlog_row_off.sql +--remove_file $MYSQLTEST_VARDIR/tmp/fk_cascade_binlog_row_on.sql + +--source include/rpl_end.inc diff --git a/mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_ordering.test b/mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_ordering.test new file mode 100644 index 0000000000000..9276eb659548d --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_ordering.test @@ -0,0 +1,69 @@ +# +# MDEV-38243 +# +# When rpl_use_binlog_events_for_fk_cascade is enabled, all cascade row events +# (both DELETE and UPDATE/SET NULL) must be written to the binary log in the +# same order the cascade operations were executed. Earlier the DELETE cascade +# events were logged immediately while UPDATE/SET NULL events were deferred to +# statement end, so every cascade delete was emitted ahead of every cascade +# update. When a single statement both SET NULLs and deletes the *same* child +# row (a child with two foreign keys to the same parent, one ON DELETE SET NULL +# and one ON DELETE CASCADE), the executed order is UPDATE-then-DELETE but the +# binlog order became DELETE-then-UPDATE. On the replica (which applies these +# events with foreign_key_checks disabled) the UPDATE event then could not find +# its row, because the DELETE had already removed it -> the SQL thread stops or +# the data diverges. +# +--source include/have_innodb.inc +--source include/have_log_bin.inc +--source include/have_binlog_format_row.inc + +--source include/master-slave.inc + +SET @old_binlog_format := @@session.binlog_format; +SET @old_rpl_use_binlog_events_for_fk_cascade := @@session.rpl_use_binlog_events_for_fk_cascade; + +SET SESSION binlog_format=ROW; +SET SESSION rpl_use_binlog_events_for_fk_cascade=1; + +CREATE TABLE p ( + id INT PRIMARY KEY +) ENGINE=InnoDB; + +# Child with two FKs to p: fk_a (SET NULL -> cascade UPDATE) is declared before +# fk_b (CASCADE -> cascade DELETE) so that on DELETE FROM p the same child row +# is first SET NULL and then deleted. +CREATE TABLE c ( + id INT PRIMARY KEY, + a INT, + b INT, + KEY(a), + KEY(b), + CONSTRAINT fk_a FOREIGN KEY (a) REFERENCES p(id) ON DELETE SET NULL, + CONSTRAINT fk_b FOREIGN KEY (b) REFERENCES p(id) ON DELETE CASCADE +) ENGINE=InnoDB; + +INSERT INTO p VALUES (1); +INSERT INTO c VALUES (10, 1, 1); + +sync_slave_with_master; +connection master; + +DELETE FROM p WHERE id=1; + +--echo # master: child row cascade-deleted +SELECT * FROM c; + +sync_slave_with_master; +connection slave; +--echo # slave must match master (row deleted); a wrong DELETE-before-UPDATE +--echo # ordering would have stopped the SQL thread on the UPDATE event +SELECT * FROM c; + +connection master; +DROP TABLE c, p; + +SET SESSION binlog_format=@old_binlog_format; +SET SESSION rpl_use_binlog_events_for_fk_cascade=@old_rpl_use_binlog_events_for_fk_cascade; + +--source include/rpl_end.inc diff --git a/mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_rollback.test b/mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_rollback.test new file mode 100644 index 0000000000000..b28060718ece0 --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_rollback.test @@ -0,0 +1,201 @@ +# +# MDEV-38243 +# +# When rpl_use_binlog_events_for_fk_cascade is enabled the master collects +# row events for FK cascade operations. If the row changes those events +# describe are rolled back, the events must be discarded (freed) and must not +# end up in the binary log. This test exercises the three rollback paths that +# discard the queued cascade events: +# +# 1. full transaction ROLLBACK (ha_rollback_trans, all=true) +# 2. ROLLBACK TO SAVEPOINT (ha_rollback_to_savepoint) +# 3. statement rollback, transaction kept (ha_rollback_trans, all=false) +# +# In every case the cascaded child row event (### UPDATE `test`.`c`) must be +# absent from the binary log, the master and slave data must stay consistent, +# and any surviving work in the same transaction must still replicate. +# +--source include/have_innodb.inc +--source include/have_log_bin.inc +--source include/have_binlog_format_row.inc + +--source include/master-slave.inc + +SET @old_binlog_format := @@session.binlog_format; +SET @old_default_storage_engine := @@session.default_storage_engine; +SET @old_rpl_use_binlog_events_for_fk_cascade := @@session.rpl_use_binlog_events_for_fk_cascade; + +SET SESSION default_storage_engine=InnoDB; +SET SESSION binlog_format=ROW; +SET SESSION rpl_use_binlog_events_for_fk_cascade=1; + +CREATE TABLE p ( + id INT PRIMARY KEY, + v INT +) ENGINE=InnoDB; + +CREATE TABLE c ( + id INT PRIMARY KEY, + pid INT, + v INT, + KEY(pid), + CONSTRAINT fk FOREIGN KEY (pid) REFERENCES p(id) + ON DELETE CASCADE + ON UPDATE CASCADE +) ENGINE=InnoDB; + +INSERT INTO p VALUES (1, 10); +INSERT INTO c VALUES (100, 1, 20); + +sync_slave_with_master; +connection master; + +--echo # +--echo # Phase 1: full transaction ROLLBACK +--echo # +FLUSH BINARY LOGS; +--let $binlog = query_get_value(SHOW MASTER STATUS, File, 1) + +BEGIN; +UPDATE p SET id=2 WHERE id=1; +ROLLBACK; + +--echo # master: parent and child unchanged +SELECT * FROM p ORDER BY id; +SELECT * FROM c ORDER BY id; + +sync_slave_with_master; +connection slave; +--echo # slave: child unchanged +SELECT * FROM c ORDER BY id; + +connection master; +FLUSH BINARY LOGS; +--let $datadir = `SELECT @@datadir` +--exec $MYSQL_BINLOG --verbose --verbose --base64-output=DECODE-ROWS $datadir/$binlog > $MYSQLTEST_VARDIR/tmp/fk_cascade_rollback_full.sql + +--echo # no cascade child row event must be present +--let SEARCH_PATTERN= ### UPDATE `test`.`c` +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/fk_cascade_rollback_full.sql +--let SEARCH_ABORT= FOUND +--source include/search_pattern_in_file.inc + +--echo # +--echo # Phase 2: ROLLBACK TO SAVEPOINT (surviving work before the savepoint) +--echo # +FLUSH BINARY LOGS; +--let $binlog = query_get_value(SHOW MASTER STATUS, File, 1) + +BEGIN; +INSERT INTO p VALUES (3, 30); +SAVEPOINT sp1; +UPDATE p SET id=2 WHERE id=1; +ROLLBACK TO SAVEPOINT sp1; +COMMIT; + +--echo # master: cascade undone, INSERT before savepoint kept +SELECT * FROM p ORDER BY id; +SELECT * FROM c ORDER BY id; + +sync_slave_with_master; +connection slave; +--echo # slave: child unchanged, parent has surviving row +SELECT * FROM p ORDER BY id; +SELECT * FROM c ORDER BY id; + +connection master; +FLUSH BINARY LOGS; +--let $datadir = `SELECT @@datadir` +--exec $MYSQL_BINLOG --verbose --verbose --base64-output=DECODE-ROWS $datadir/$binlog > $MYSQLTEST_VARDIR/tmp/fk_cascade_rollback_sp.sql + +--echo # cascade child row event must be absent +--let SEARCH_PATTERN= ### UPDATE `test`.`c` +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/fk_cascade_rollback_sp.sql +--let SEARCH_ABORT= FOUND +--source include/search_pattern_in_file.inc + +--echo # surviving INSERT before the savepoint must be replicated +--let SEARCH_PATTERN= ### INSERT INTO `test`.`p` +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/fk_cascade_rollback_sp.sql +--let SEARCH_ABORT= NOT FOUND +--source include/search_pattern_in_file.inc + +DROP TABLE c, p; + +--echo # +--echo # Phase 3: statement rollback, transaction continues and commits +--echo # +# The parent has a secondary UNIQUE key. A multi-row UPDATE cascades on the +# first row (queuing a child cascade event) and then hits a duplicate-key +# error on the second row, which rolls back the statement. The transaction +# stays open and commits later work. The discarded cascade event must not be +# flushed into the binary log at COMMIT. +CREATE TABLE p ( + id INT PRIMARY KEY, + u INT, + UNIQUE KEY uq_u(u) +) ENGINE=InnoDB; + +CREATE TABLE c ( + cid INT PRIMARY KEY, + pid INT, + KEY(pid), + CONSTRAINT fk FOREIGN KEY (pid) REFERENCES p(id) + ON UPDATE CASCADE +) ENGINE=InnoDB; + +INSERT INTO p VALUES (1, 100), (2, 200); +INSERT INTO c VALUES (11, 1); + +sync_slave_with_master; +connection master; + +FLUSH BINARY LOGS; +--let $binlog = query_get_value(SHOW MASTER STATUS, File, 1) + +BEGIN; +# row id=1: id 1->11 cascades c.pid 1->11 (event queued), u 100->100 (no change) +# row id=2: u 200->100 duplicates row 1's u -> ER_DUP_ENTRY -> statement rollback +--error ER_DUP_ENTRY +UPDATE p SET id=id+10, u=100 WHERE id IN (1,2) ORDER BY id; +INSERT INTO p VALUES (9, 900); +COMMIT; + +--echo # master: failed UPDATE rolled back, INSERT committed +SELECT * FROM p ORDER BY id; +SELECT * FROM c ORDER BY cid; + +sync_slave_with_master; +connection slave; +--echo # slave: child unchanged, parent has committed row +SELECT * FROM p ORDER BY id; +SELECT * FROM c ORDER BY cid; + +connection master; +FLUSH BINARY LOGS; +--let $datadir = `SELECT @@datadir` +--exec $MYSQL_BINLOG --verbose --verbose --base64-output=DECODE-ROWS $datadir/$binlog > $MYSQLTEST_VARDIR/tmp/fk_cascade_rollback_stmt.sql + +--echo # discarded cascade child row event must be absent +--let SEARCH_PATTERN= ### UPDATE `test`.`c` +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/fk_cascade_rollback_stmt.sql +--let SEARCH_ABORT= FOUND +--source include/search_pattern_in_file.inc + +--echo # committed INSERT must be replicated +--let SEARCH_PATTERN= ### INSERT INTO `test`.`p` +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/fk_cascade_rollback_stmt.sql +--let SEARCH_ABORT= NOT FOUND +--source include/search_pattern_in_file.inc + +DROP TABLE c, p; + +SET SESSION default_storage_engine=@old_default_storage_engine; +SET SESSION binlog_format=@old_binlog_format; +SET SESSION rpl_use_binlog_events_for_fk_cascade=@old_rpl_use_binlog_events_for_fk_cascade; + +--remove_file $MYSQLTEST_VARDIR/tmp/fk_cascade_rollback_full.sql +--remove_file $MYSQLTEST_VARDIR/tmp/fk_cascade_rollback_sp.sql +--remove_file $MYSQLTEST_VARDIR/tmp/fk_cascade_rollback_stmt.sql + +--source include/rpl_end.inc diff --git a/mysql-test/suite/rpl/t/rpl_fk_set_null_binlog_row.test b/mysql-test/suite/rpl/t/rpl_fk_set_null_binlog_row.test new file mode 100644 index 0000000000000..42c7278e45ee3 --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_fk_set_null_binlog_row.test @@ -0,0 +1,105 @@ +--source include/have_innodb.inc +--source include/have_log_bin.inc +--source include/have_binlog_format_row.inc + +--source include/master-slave.inc + +SET @old_binlog_format := @@session.binlog_format; +SET @old_default_storage_engine := @@session.default_storage_engine; +SET @old_rpl_use_binlog_events_for_fk_cascade := @@session.rpl_use_binlog_events_for_fk_cascade; + +SET SESSION default_storage_engine=InnoDB; +SET SESSION binlog_format=ROW; + +# Phase A: feature OFF (no explicit FK SET NULL row events in binlog) +SET SESSION rpl_use_binlog_events_for_fk_cascade=0; + +CREATE TABLE p ( + id INT PRIMARY KEY, + v INT +) ENGINE=InnoDB; + +CREATE TABLE c ( + id INT PRIMARY KEY, + pid INT NULL, + v INT, + KEY(pid), + CONSTRAINT fk FOREIGN KEY (pid) REFERENCES p(id) + ON DELETE SET NULL + ON UPDATE SET NULL +) ENGINE=InnoDB; + +INSERT INTO p VALUES (1, 10); +INSERT INTO c VALUES (100, 1, 20); + +UPDATE p SET id=2 WHERE id=1; + +sync_slave_with_master; +SELECT pid FROM c ORDER BY id; + +connection master; + +--let $binlog = query_get_value(SHOW MASTER STATUS, File, 1) +--let $datadir = `SELECT @@datadir` + +FLUSH BINARY LOGS; + +--exec $MYSQL_BINLOG --verbose --verbose --base64-output=DECODE-ROWS $datadir/$binlog > $MYSQLTEST_VARDIR/tmp/fk_set_null_binlog_row_off.sql + +--let SEARCH_PATTERN= ### UPDATE `test`.`c` +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/fk_set_null_binlog_row_off.sql +--let SEARCH_ABORT= FOUND +--source include/search_pattern_in_file.inc + +DROP TABLE c, p; + +# Phase B: feature ON (explicit FK SET NULL row events in binlog) +SET SESSION rpl_use_binlog_events_for_fk_cascade=1; + +CREATE TABLE p ( + id INT PRIMARY KEY, + v INT +) ENGINE=InnoDB; + +CREATE TABLE c ( + id INT PRIMARY KEY, + pid INT NULL, + v INT, + KEY(pid), + CONSTRAINT fk FOREIGN KEY (pid) REFERENCES p(id) + ON DELETE SET NULL + ON UPDATE SET NULL +) ENGINE=InnoDB; + +INSERT INTO p VALUES (1, 10); +INSERT INTO c VALUES (100, 1, 20); + +UPDATE p SET id=2 WHERE id=1; + +sync_slave_with_master; +SELECT pid FROM c ORDER BY id; + +connection master; + +--let $binlog = query_get_value(SHOW MASTER STATUS, File, 1) +--let $datadir = `SELECT @@datadir` + +FLUSH BINARY LOGS; + +--exec $MYSQL_BINLOG --verbose --verbose --base64-output=DECODE-ROWS $datadir/$binlog > $MYSQLTEST_VARDIR/tmp/fk_set_null_binlog_row_on.sql + +--let SEARCH_PATTERN= ### UPDATE `test`.`c` +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/fk_set_null_binlog_row_on.sql +--let SEARCH_ABORT= NOT FOUND|FOUND 0|FOUND [2-9] +--source include/search_pattern_in_file.inc + +DROP TABLE c, p; + +SET SESSION default_storage_engine=@old_default_storage_engine; +SET SESSION binlog_format=@old_binlog_format; +SET SESSION rpl_use_binlog_events_for_fk_cascade=@old_rpl_use_binlog_events_for_fk_cascade; + +--remove_file $MYSQLTEST_VARDIR/tmp/fk_set_null_binlog_row_off.sql +--remove_file $MYSQLTEST_VARDIR/tmp/fk_set_null_binlog_row_on.sql + +--source include/rpl_end.inc diff --git a/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result b/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result index 089c261570542..55b71578dac0b 100644 --- a/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result +++ b/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result @@ -3472,6 +3472,16 @@ NUMERIC_BLOCK_SIZE 1 ENUM_VALUE_LIST NULL READ_ONLY NO COMMAND_LINE_ARGUMENT REQUIRED +VARIABLE_NAME RPL_USE_BINLOG_EVENTS_FOR_FK_CASCADE +VARIABLE_SCOPE SESSION +VARIABLE_TYPE BOOLEAN +VARIABLE_COMMENT If enabled, the master will write row events for foreign key cascade operations +NUMERIC_MIN_VALUE NULL +NUMERIC_MAX_VALUE NULL +NUMERIC_BLOCK_SIZE NULL +ENUM_VALUE_LIST OFF,ON +READ_ONLY NO +COMMAND_LINE_ARGUMENT OPTIONAL VARIABLE_NAME SECURE_FILE_PRIV VARIABLE_SCOPE GLOBAL VARIABLE_TYPE VARCHAR diff --git a/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result b/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result index 330e276489ee1..44fb97bb74d4e 100644 --- a/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result +++ b/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result @@ -4042,6 +4042,16 @@ NUMERIC_BLOCK_SIZE 1 ENUM_VALUE_LIST NULL READ_ONLY NO COMMAND_LINE_ARGUMENT REQUIRED +VARIABLE_NAME RPL_USE_BINLOG_EVENTS_FOR_FK_CASCADE +VARIABLE_SCOPE SESSION +VARIABLE_TYPE BOOLEAN +VARIABLE_COMMENT If enabled, the master will write row events for foreign key cascade operations +NUMERIC_MIN_VALUE NULL +NUMERIC_MAX_VALUE NULL +NUMERIC_BLOCK_SIZE NULL +ENUM_VALUE_LIST OFF,ON +READ_ONLY NO +COMMAND_LINE_ARGUMENT OPTIONAL VARIABLE_NAME SECURE_FILE_PRIV VARIABLE_SCOPE GLOBAL VARIABLE_TYPE VARCHAR diff --git a/sql/handler.cc b/sql/handler.cc index c915ccabc28f9..a7d837a5b7049 100644 --- a/sql/handler.cc +++ b/sql/handler.cc @@ -118,6 +118,35 @@ static handlerton *installed_htons[128]; KEY_CREATE_INFO default_key_create_info= { HA_KEY_ALG_UNDEF, 0, 0, {NullS, 0}, {NullS, 0}, false }; +static void flush_pending_cascade_binlog_for_thd(THD *thd) +{ + if (!thd || thd->rgi_slave) return; + + TABLE *table; + for (table = thd->open_tables; table; table = table->next) { + if (table->file) { + table->file->flush_pending_cascade_binlog(); + } + } +} + +static void discard_pending_cascade_binlog_for_thd(THD *thd) +{ + /* + Unlike flushing, discarding is pure cleanup: it only frees the queued + cascade row events, it never writes to the binary log. So it must run for + slave/applier threads too. Hence no thd->rgi_slave check here. + */ + if (!thd) return; + + TABLE *table; + for (table = thd->open_tables; table; table = table->next) { + if (table->file) { + table->file->discard_pending_cascade_binlog(); + } + } +} + /* number of entries in handlertons[] */ ulong total_ha= 0; /* number of storage engines (from handlertons[]) that support 2pc */ @@ -2142,6 +2171,14 @@ int ha_commit_trans(THD *thd, bool all) thd->mdl_context.release_lock(thd->backup_commit_lock); thd->backup_commit_lock= 0; } + + if (!error) + { + if ((thd->variables.rpl_use_binlog_events_for_fk_cascade || + WSREP_EMULATE_BINLOG(thd)) && + (all || thd->in_active_multi_stmt_transaction())) + flush_pending_cascade_binlog_for_thd(thd); + } #ifdef WITH_WSREP if (wsrep_is_active(thd) && is_real_trans && !error && (rw_ha_count == 0 || all) && @@ -2335,12 +2372,11 @@ int ha_rollback_trans(THD *thd, bool all) rollback without signalling following transactions. And in release builds, we explicitly do the signalling before rolling back. */ - DBUG_ASSERT( - !(thd->rgi_slave && - !thd->rgi_slave->worker_error && - thd->rgi_slave->did_mark_start_commit) || - (thd->transaction->xid_state.is_explicit_XA() || - (thd->rgi_slave->gtid_ev_flags2 & Gtid_log_event::FL_PREPARED_XA))); + if (!(thd->rgi_slave && thd->rgi_slave->worker_error)) + DBUG_ASSERT( + !(thd->rgi_slave && thd->rgi_slave->did_mark_start_commit) || + (thd->transaction->xid_state.is_explicit_XA() || + (thd->rgi_slave->gtid_ev_flags2 & Gtid_log_event::FL_PREPARED_XA))); if (thd->rgi_slave && !thd->rgi_slave->worker_error && @@ -2422,6 +2458,19 @@ int ha_rollback_trans(THD *thd, bool all) binlog_post_rollback(thd, all); } + /* + The engines have rolled back (the whole transaction when all==true, or the + current statement to its implicit savepoint when all==false). Any queued + FK-cascade row events describe row changes that were just undone, so + discard and free them rather than letting them be flushed into the binary + log by a later statement or at commit. For a full rollback the queue would + also be freed when the trx is released, but discarding here keeps the + statement-rollback case correct and the behaviour consistent. + */ + if (thd->variables.rpl_use_binlog_events_for_fk_cascade || + WSREP_EMULATE_BINLOG(thd)) + discard_pending_cascade_binlog_for_thd(thd); + #ifdef WITH_WSREP if (WSREP(thd) && thd->is_error()) { @@ -3342,6 +3391,18 @@ int ha_rollback_to_savepoint(THD *thd, SAVEPOINT *sv) my_error(ER_ERROR_DURING_ROLLBACK, MYF(0), err); error=1; } + + /* + The cascade row events queued for this transaction describe row + changes that are being rolled back to the savepoint (or the queue is + empty because earlier statements already flushed their events into the + binlog cache, which the binlog savepoint machinery truncates + separately). Either way we must not write these events, so discard and + free them rather than flushing them into the binary log. + */ + if (thd->variables.rpl_use_binlog_events_for_fk_cascade || + WSREP_EMULATE_BINLOG(thd)) + discard_pending_cascade_binlog_for_thd(thd); #ifdef WITH_WSREP if (WSREP(thd) && ht->flags & HTON_WSREP_REPLICATION) { diff --git a/sql/handler.h b/sql/handler.h index fa9196f189a8f..a2f85ac0303d9 100644 --- a/sql/handler.h +++ b/sql/handler.h @@ -5310,7 +5310,18 @@ class handler :public Sql_alloc bool check_table_binlog_row_based(); bool prepare_for_row_logging(); int prepare_for_modify(bool can_set_fields, bool can_lookup); - int binlog_log_row(const uchar *before_record, const uchar *after_record, + + virtual void flush_pending_cascade_binlog() {} + + /* + Discard FK-cascade row events queued for this transaction, + when transaction rolls back. + */ + virtual void discard_pending_cascade_binlog() {} + + int prepare_for_insert(bool do_create); + int binlog_log_row(const uchar *before_record, + const uchar *after_record, Log_func *log_func); inline void clear_cached_table_binlog_row_based_flag() diff --git a/sql/log.cc b/sql/log.cc index 387795e30fce7..2c155fda778b7 100644 --- a/sql/log.cc +++ b/sql/log.cc @@ -8068,7 +8068,27 @@ int binlog_flush_pending_rows_event(THD *thd, bool stmt_end, */ if (stmt_end) { - pending->set_flags(Rows_log_event::STMT_END_F); + if (thd->binlog_fk_cascade_events && + !thd->rgi_slave && + (thd->variables.rpl_use_binlog_events_for_fk_cascade || + WSREP_EMULATE_BINLOG(thd))) + { + TABLE *table; + for (table= thd->open_tables; table; table= table->next) + { + if (table->file) + table->file->flush_pending_cascade_binlog(); + } + } + + /* + Flushing cascaded row events may have created a new pending event or + replaced the current one. Ensure we mark the final pending event as + statement end. + */ + pending= cache_data->pending(); + if (pending) + pending->set_flags(Rows_log_event::STMT_END_F); thd->reset_binlog_for_next_statement(); } @@ -8294,6 +8314,9 @@ Event_log::prepare_pending_rows_event(THD *thd, TABLE* table, if (unlikely(!ev)) DBUG_RETURN(NULL); ev->server_id= serv_id; // I don't like this, it's too easy to forget. + + if (thd->binlog_fk_cascade_events) + ev->set_flags(Rows_log_event::FK_CASCADE_EVENTS_F); /* flush the pending event and replace it with the newly created event... diff --git a/sql/log_event.h b/sql/log_event.h index 43a3b7ff76021..c794b35e7ee04 100644 --- a/sql/log_event.h +++ b/sql/log_event.h @@ -4792,6 +4792,8 @@ class Rows_log_event : public Log_event */ COMPLETE_ROWS_F = (1U << 3), + FK_CASCADE_EVENTS_F = (1U << 4), + /* Value of the OPTION_NO_CHECK_CONSTRAINT_CHECKS flag in thd->options */ NO_CHECK_CONSTRAINT_CHECKS_F = (1U << 7) }; diff --git a/sql/log_event_server.cc b/sql/log_event_server.cc index 1c0ed551f27d9..9e64cf5eb2f64 100644 --- a/sql/log_event_server.cc +++ b/sql/log_event_server.cc @@ -5036,7 +5036,13 @@ int Rows_log_event::do_apply_event(rpl_group_info *rgi) Make sure to set/clear them before executing the main body of the event. */ - if (get_flags(NO_FOREIGN_KEY_CHECKS_F)) + /* + FK_CASCADE_EVENTS_F marks row events that the master emitted for the + changes performed by cascading foreign key operations. The applier must + not re-run the cascade, so foreign key checks are disabled for these + events just as they are for NO_FOREIGN_KEY_CHECKS_F. + */ + if (get_flags(NO_FOREIGN_KEY_CHECKS_F) || get_flags(FK_CASCADE_EVENTS_F)) thd->variables.option_bits|= OPTION_NO_FOREIGN_KEY_CHECKS; else thd->variables.option_bits&= ~OPTION_NO_FOREIGN_KEY_CHECKS; diff --git a/sql/service_wsrep.cc b/sql/service_wsrep.cc index 3f09e9d8b3e47..705d64921c372 100644 --- a/sql/service_wsrep.cc +++ b/sql/service_wsrep.cc @@ -441,3 +441,8 @@ extern "C" my_bool wsrep_thd_is_local_transaction(const THD *thd) return (wsrep_thd_is_local(thd) && thd->wsrep_cs().transaction().active()); } + +extern "C" my_bool wsrep_emulate_binlog(const THD *thd) +{ + return WSREP_EMULATE_BINLOG_NNULL(thd); +} diff --git a/sql/sql_class.cc b/sql/sql_class.cc index 47de84e48d131..b833e0de3e45b 100644 --- a/sql/sql_class.cc +++ b/sql/sql_class.cc @@ -93,6 +93,22 @@ extern "C" const uchar *get_var_key(const void *entry_, size_t *length, return reinterpret_cast(entry->name.str); } +void THD::binlog_mark_fk_cascade_events() +{ + binlog_fk_cascade_events= true; + + if (binlog_cache_mngr *cache_mngr= binlog_get_cache_mngr()) + { + if (Rows_log_event *pending= binlog_get_pending_rows_event( + cache_mngr, use_trans_cache(this, false))) + pending->set_flags(Rows_log_event::FK_CASCADE_EVENTS_F); + + if (Rows_log_event *pending= binlog_get_pending_rows_event( + cache_mngr, use_trans_cache(this, true))) + pending->set_flags(Rows_log_event::FK_CASCADE_EVENTS_F); + } +} + extern "C" void free_user_var(void *entry_) { user_var_entry *entry= static_cast(entry_); @@ -507,6 +523,47 @@ int thd_sql_command(const THD *thd) return (int) thd->lex->sql_command; } +extern "C" +int thd_is_current_stmt_binlog_format_row(const THD *thd) +{ + return (int) thd->is_current_stmt_binlog_format_row(); +} + +extern "C" +int thd_rpl_use_binlog_events_for_fk_cascade(const THD *thd) +{ + return (int) thd->variables.rpl_use_binlog_events_for_fk_cascade; +} + +extern "C" +void thd_binlog_mark_fk_cascade_events(THD *thd) +{ + thd->binlog_mark_fk_cascade_events(); +} + +extern "C" +int thd_binlog_update_row(THD *thd, TABLE *table, Event_log *bin_log, + binlog_cache_data *cache_data, int is_trans, + unsigned long row_image, + const unsigned char *before_record, + const unsigned char *after_record) +{ + return thd->binlog_update_row(table, bin_log, cache_data, (bool) is_trans, + (enum_binlog_row_image) row_image, + before_record, after_record); +} + +extern "C" +int thd_binlog_delete_row(THD *thd, TABLE *table, Event_log *bin_log, + binlog_cache_data *cache_data, int is_trans, + unsigned long row_image, + const unsigned char *before_record) +{ + return thd->binlog_delete_row(table, bin_log, cache_data, (bool) is_trans, + (enum_binlog_row_image) row_image, + before_record); +} + /* Returns options used with DDL's, like IF EXISTS etc... Will returns 'nonsense' if the command was not a DDL. @@ -902,6 +959,7 @@ THD::THD(my_thread_id id, bool is_wsrep_applier) mysys_var=0; binlog_evt_union.do_union= FALSE; binlog_table_maps= FALSE; + binlog_fk_cascade_events= FALSE; binlog_xid= 0; enable_slow_log= 0; durability_property= HA_REGULAR_DURABILITY; diff --git a/sql/sql_class.h b/sql/sql_class.h index f660197f46d76..f768b7444ed92 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -917,6 +917,7 @@ typedef struct system_variables my_bool sql_log_bin; my_bool binlog_annotate_row_events; my_bool binlog_direct_non_trans_update; + my_bool rpl_use_binlog_events_for_fk_cascade; my_bool column_compression_zlib_wrap; my_bool sysdate_is_now; my_bool wsrep_on; @@ -3853,6 +3854,10 @@ class THD: public THD_count, /* this must be first */ /* 1 if binlog table maps has been written */ bool binlog_table_maps; + bool binlog_fk_cascade_events; + + void binlog_mark_fk_cascade_events(); + void issue_unsafe_warnings(); void reset_unsafe_warnings() { binlog_unsafe_warning_flags= 0; } @@ -3860,6 +3865,7 @@ class THD: public THD_count, /* this must be first */ void reset_binlog_for_next_statement() { binlog_table_maps= 0; + binlog_fk_cascade_events= false; } bool binlog_table_should_be_logged(const LEX_CSTRING *db); diff --git a/sql/sys_vars.cc b/sql/sys_vars.cc index 08f2abac210a0..c36ac7b19af49 100644 --- a/sql/sys_vars.cc +++ b/sql/sys_vars.cc @@ -781,6 +781,26 @@ Sys_binlog_create_tmptable_format( NO_MUTEX_GUARD, NOT_IN_BINLOG, ON_CHECK(binlog_create_tmp_format_check)); +static bool rpl_use_binlog_events_for_fk_cascade_check(sys_var *self, THD *thd, set_var *var) +{ + if (var->type == OPT_GLOBAL) + return false; + + if (unlikely(error_if_in_trans_or_substatement(thd, + ER_STORED_FUNCTION_PREVENTS_SWITCH_SQL_LOG_BIN, + ER_INSIDE_TRANSACTION_PREVENTS_SWITCH_SQL_LOG_BIN))) + return true; + + return false; +} + +static Sys_var_mybool Sys_rpl_use_binlog_events_for_fk_cascade( + "rpl_use_binlog_events_for_fk_cascade", + "If enabled, the master will write row events for foreign key cascade operations", + SESSION_VAR(rpl_use_binlog_events_for_fk_cascade), + CMD_LINE(OPT_ARG), DEFAULT(FALSE), + NO_MUTEX_GUARD, NOT_IN_BINLOG, ON_CHECK(rpl_use_binlog_events_for_fk_cascade_check)); + static bool deprecated_explicit_defaults_for_timestamp(sys_var *self, THD *thd, set_var *var) { diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc index b60155f995683..4c373b5ac2baf 100644 --- a/storage/innobase/handler/ha_innodb.cc +++ b/storage/innobase/handler/ha_innodb.cc @@ -49,8 +49,10 @@ this program; if not, write to the Free Software Foundation, Inc., #include #include #include -#include +#include +#include #include +#include #include #include "sql_type_geom.h" #include "scope.h" @@ -8870,6 +8872,88 @@ ha_innobase::update_row( DBUG_RETURN(err); } +void +ha_innobase::flush_pending_cascade_binlog() +{ + if (!m_user_thd) { + return; + } + trx_t* trx = thd_to_trx(m_user_thd); + const bool emulate_binlog= +#ifdef WITH_WSREP + wsrep_emulate_binlog(m_user_thd); +#else + false; +#endif + + if (!thd_rpl_use_binlog_events_for_fk_cascade(m_user_thd) && + !emulate_binlog) { + return; + } + + if (trx == NULL || trx->pending_cascade_binlog_row_events.empty()) { + return; + } + + m_user_thd->binlog_mark_fk_cascade_events(); + + for (auto& ev : trx->pending_cascade_binlog_row_events) { + if (ev.table == NULL || ev.table->file == NULL) { + if (ev.before_record) { + my_free(ev.before_record); + } + if (ev.after_record) { + my_free(ev.after_record); + } + continue; + } + + if (ev.table->s->tmp_table != NO_TMP_TABLE) { + if (ev.before_record) { + my_free(ev.before_record); + } + if (ev.after_record) { + my_free(ev.after_record); + } + continue; + } + + MY_BITMAP* old_read_set = ev.table->read_set; + MY_BITMAP* old_write_set = ev.table->write_set; + MY_BITMAP* old_rpl_write_set = ev.table->rpl_write_set; + + ev.table->column_bitmaps_set_no_signal( + &ev.table->s->all_set, &ev.table->s->all_set); + if (ev.table->rpl_write_set == NULL) { + ev.table->rpl_write_set = &ev.table->s->all_set; + } + + Log_func* log_func = reinterpret_cast(ev.log_func); + ev.table->file->binlog_log_row(ev.before_record, + ev.after_record, + log_func); + + ev.table->column_bitmaps_set_no_signal(old_read_set, old_write_set); + ev.table->rpl_write_set = old_rpl_write_set; + + my_free(ev.before_record); + my_free(ev.after_record); + } + + trx->pending_cascade_binlog_row_events.clear(); +} + +void +ha_innobase::discard_pending_cascade_binlog() +{ + if (!m_user_thd) { + return; + } + if (trx_t* trx = thd_to_trx(m_user_thd)) { + trx->free_cascade_binlog_row_events(); + } +} + /**********************************************************************//** Deletes a row given as the parameter. @return error number or 0 */ diff --git a/storage/innobase/handler/ha_innodb.h b/storage/innobase/handler/ha_innodb.h index d47a52e5061ea..8b2c165eb0e0b 100644 --- a/storage/innobase/handler/ha_innodb.h +++ b/storage/innobase/handler/ha_innodb.h @@ -121,6 +121,10 @@ class ha_innobase final : public handler int update_row(const uchar * old_data, const uchar * new_data) override; + void flush_pending_cascade_binlog() override; + + void discard_pending_cascade_binlog() override; + int delete_row(const uchar * buf) override; bool was_semi_consistent_read() override; @@ -517,6 +521,20 @@ class ha_innobase final : public handler /** Save CPU time with prebuilt/cached data structures */ row_prebuilt_t* m_prebuilt; +public: + row_prebuilt_t* innobase_prebuilt() const { return m_prebuilt; } + + void rebuild_template_for_cascade_binlog_row_image() + { + reset_template(); + build_template(true); + } + + void reset_template_for_cascade_binlog_row_image() + { + reset_template(); + } + /** Thread handle of the user currently using the handler; this is set in external_lock function */ THD* m_user_thd; diff --git a/storage/innobase/include/row0sel.h b/storage/innobase/include/row0sel.h index 8939fe704a3cf..7c37bd06e40d6 100644 --- a/storage/innobase/include/row0sel.h +++ b/storage/innobase/include/row0sel.h @@ -27,7 +27,7 @@ Created 12/19/1997 Heikki Tuuri #pragma once #include "data0data.h" -#include "que0types.h" +#include "dict0dict.h" #include "trx0types.h" #include "read0types.h" #include "row0types.h" @@ -37,6 +37,16 @@ Created 12/19/1997 Heikki Tuuri #include "row0mysql.h" #include "row0query.h" +MY_ATTRIBUTE((warn_unused_result)) +bool row_sel_store_mysql_rec( + byte* mysql_rec, + row_prebuilt_t* prebuilt, + const rec_t* rec, + const dtuple_t* vrow, + bool rec_clust, + const dict_index_t* index, + const rec_offs* offsets); + /*********************************************************************//** Creates a select node struct. @return own: select node struct */ diff --git a/storage/innobase/include/trx0trx.h b/storage/innobase/include/trx0trx.h index bef9311b81b32..5dc88011f9cbe 100644 --- a/storage/innobase/include/trx0trx.h +++ b/storage/innobase/include/trx0trx.h @@ -44,6 +44,14 @@ Created 3/26/1996 Heikki Tuuri struct mtr_t; struct rw_trx_hash_element_t; class ha_handler_stats; +struct TABLE; + +struct trx_cascade_binlog_row_event { + TABLE* table; + unsigned char* before_record; + unsigned char* after_record; + void* log_func; +}; /******************************************************************//** Set detailed error message for the transaction. */ @@ -962,6 +970,13 @@ struct trx_t : ilist_node<> transaction branch */ trx_mod_tables_t mod_tables; /*!< List of tables that were modified by this transaction */ + + std::vector pending_cascade_binlog_row_events; + + /** Free the events of any queued FK-cascade binlog row events and empty + the list. Used both to discard events whose row changes are being rolled + back and to reclaim memory for events that were never flushed. */ + void free_cascade_binlog_row_events(); /*------------------------------*/ char* detailed_error; /*!< detailed error message for last error, or empty. */ diff --git a/storage/innobase/row/row0ins.cc b/storage/innobase/row/row0ins.cc index 1920289945f27..bfdec70780b63 100644 --- a/storage/innobase/row/row0ins.cc +++ b/storage/innobase/row/row0ins.cc @@ -40,6 +40,10 @@ Created 4/20/1996 Heikki Tuuri #include "log0log.h" #include "eval0eval.h" #include "data0data.h" +#include "log0recv.h" +#include "handler.h" +#include "table.h" +#include "ha_innodb.h" #include "buf0lru.h" #include "fts0fts.h" #include "fts0types.h" @@ -47,12 +51,96 @@ Created 4/20/1996 Heikki Tuuri # include "btr0sea.h" #endif #include "sql_class.h" // THD +#include +#include #ifdef WITH_WSREP #include #include #include "ha_prototypes.h" #endif /* WITH_WSREP */ +TABLE *find_fk_open_table(THD *thd, const char *db, size_t db_len, + const char *table, size_t table_len); + +extern "C" bool thd_is_slave(const MYSQL_THD thd); + +static bool row_ins_fk_cascade_delete_binlog_row(THD *thd, TABLE *table, + Event_log *bin_log, + binlog_cache_data *cache_data, + bool is_transactional, + ulong row_image, + const uchar *before_record, + const uchar *after_record + __attribute__((unused))) +{ + return thd_binlog_delete_row(thd, table, bin_log, cache_data, + (int) is_transactional, row_image, + before_record); +} + +static bool row_ins_fk_cascade_update_binlog_row(THD *thd, TABLE *table, + Event_log *bin_log, + binlog_cache_data *cache_data, + bool is_transactional, + ulong row_image, + const uchar *before_record, + const uchar *after_record) +{ + return thd_binlog_update_row(thd, table, bin_log, cache_data, + (int) is_transactional, row_image, + before_record, after_record); +} + +static TABLE* +row_ins_find_open_table_for_cascade_binlog( + trx_t* trx, + dict_table_t* child) +{ + THD* thd; + TABLE* mysql_table; + char db_buf[NAME_LEN + 1]; + char tbl_buf[NAME_LEN + 1]; + ulint db_buf_len; + ulint tbl_buf_len; + + thd = trx->mysql_thd; + if (thd == NULL) { + return NULL; + } + + if (!child->parse_name(db_buf, tbl_buf, &db_buf_len, &tbl_buf_len)) { + return NULL; + } + + mysql_table = find_fk_open_table(thd, + db_buf, db_buf_len, + tbl_buf, tbl_buf_len); + + return mysql_table; +} + +static inline bool +row_ins_allow_fk_cascade_binlog_for_table(const TABLE* table) +{ + if (table == NULL) { + return false; + } + + if (table->s->primary_key != MAX_KEY) { + return true; + } + + if (Field **vf = table->vfield) { + for (; *vf; vf++) { + if ((*vf)->flags & PART_KEY_FLAG) { + return false; + } + } + } + + return true; +} + /************************************************************************* IMPORTANT NOTE: Any operation that generates redo MUST check that there is enough space in the redo log before for that operation. This is @@ -269,7 +357,7 @@ static MY_ATTRIBUTE((nonnull, warn_unused_result)) dberr_t row_ins_clust_index_entry_by_modify( /*================================*/ - btr_pcur_t* pcur, /*!< in/out: a persistent cursor pointing + btr_pcur_t* pcur, /*!< in/out: a persistent cursor pointing to the clust_rec that is being modified. */ ulint flags, /*!< in: undo logging and locking flags */ ulint mode, /*!< in: BTR_MODIFY_LEAF or BTR_MODIFY_TREE, @@ -299,7 +387,6 @@ row_ins_clust_index_entry_by_modify( /* In delete-marked records, DB_TRX_ID must always refer to an existing undo log record. */ ut_ad(rec_get_trx_id(rec, cursor->index())); - /* Build an update vector containing all the fields to be modified; NOTE that this vector may NOT contain system columns trx_id or roll_ptr */ @@ -1012,6 +1099,13 @@ row_ins_foreign_check_on_constraint( mem_heap_t* tmp_heap = NULL; doc_id_t doc_id = FTS_NULL_DOC_ID; + TABLE* child_mysql_table = NULL; + byte* before_mysql_rec = NULL; + byte* after_mysql_rec = NULL; + bool need_cascade_binlog = false; + bool can_cascade_binlog = false; + bool have_after_image = false; + DBUG_ENTER("row_ins_foreign_check_on_constraint"); trx = thr_get_trx(thr); @@ -1346,15 +1440,213 @@ row_ins_foreign_check_on_constraint( cascade->state = UPD_NODE_UPDATE_CLUSTERED; + /* + Don't build cascade binlog events on a slave thread, + */ + if (trx->mysql_thd && + !thd_is_slave(trx->mysql_thd) && + thd_rpl_use_binlog_events_for_fk_cascade(trx->mysql_thd)) { + + child_mysql_table = row_ins_find_open_table_for_cascade_binlog(trx, table); + if (child_mysql_table != NULL) { + handler* file = child_mysql_table->file; + const bool allow_rpl_fk_cascade_binlog= + row_ins_allow_fk_cascade_binlog_for_table(child_mysql_table); + const bool emulate_binlog= +#ifdef WITH_WSREP + wsrep_emulate_binlog(trx->mysql_thd); +#else + false; +#endif + if (child_mysql_table->in_use == trx->mysql_thd + && (emulate_binlog || + thd_is_current_stmt_binlog_format_row(trx->mysql_thd)) + && ((emulate_binlog) || + (thd_rpl_use_binlog_events_for_fk_cascade(trx->mysql_thd) && + allow_rpl_fk_cascade_binlog)) + && file->prepare_for_row_logging()) { + need_cascade_binlog = true; + before_mysql_rec = static_cast( + mem_heap_alloc(tmp_heap, child_mysql_table->s->reclength)); + if (cascade->is_delete != PLAIN_DELETE) { + after_mysql_rec = static_cast( + mem_heap_alloc(tmp_heap, child_mysql_table->s->reclength)); + } + } + } + + if (need_cascade_binlog) { + ha_innobase* ib = static_cast(child_mysql_table->file); + row_prebuilt_t* prebuilt = ib->innobase_prebuilt(); + if (prebuilt != NULL && prebuilt->mysql_template != NULL) { + MY_BITMAP* old_read_set = child_mysql_table->read_set; + MY_BITMAP* old_write_set = child_mysql_table->write_set; + MY_BITMAP* old_rpl_write_set = child_mysql_table->rpl_write_set; + child_mysql_table->column_bitmaps_set_no_signal( + &child_mysql_table->tmp_set, &child_mysql_table->tmp_set); + bitmap_set_all(&child_mysql_table->tmp_set); + if (Field **vf = child_mysql_table->vfield) { + for (; *vf; vf++) { + bitmap_clear_bit(&child_mysql_table->tmp_set, (*vf)->field_index); + } + } + if (child_mysql_table->rpl_write_set == NULL) { + child_mysql_table->rpl_write_set = &child_mysql_table->tmp_set; + } + ib->rebuild_template_for_cascade_binlog_row_image(); + + mtr_start(mtr); + if (cascade->pcur->restore_position(BTR_SEARCH_LEAF, mtr) + == btr_pcur_t::SAME_ALL) { + const rec_t* before_rec = btr_pcur_get_rec(cascade->pcur); + mem_heap_t* offs_heap = NULL; + rec_offs offsets_[REC_OFFS_NORMAL_SIZE]; + rec_offs_init(offsets_); + const rec_offs* offsets = rec_get_offsets( + before_rec, clust_index, offsets_, clust_index->n_core_fields, + ULINT_UNDEFINED, &offs_heap); + dict_index_t* saved_index = prebuilt->index; + prebuilt->index = clust_index; + if (!row_sel_store_mysql_rec(before_mysql_rec, prebuilt, + before_rec, NULL, true, + clust_index, offsets)) { + need_cascade_binlog = false; + } + prebuilt->index = saved_index; + if (UNIV_LIKELY_NULL(offs_heap)) { + mem_heap_free(offs_heap); + } + } else { + need_cascade_binlog = false; + } + mtr_commit(mtr); + + child_mysql_table->column_bitmaps_set_no_signal(old_read_set, old_write_set); + child_mysql_table->rpl_write_set = old_rpl_write_set; + ib->reset_template_for_cascade_binlog_row_image(); + } else { + need_cascade_binlog = false; + } + } + } err = row_update_cascade_for_mysql(thr, cascade, foreign->foreign_table); mtr_start(mtr); + can_cascade_binlog = (err == DB_SUCCESS && need_cascade_binlog); + have_after_image = false; + + if (can_cascade_binlog && cascade->is_delete != PLAIN_DELETE) { + if (cascade->pcur->restore_position(BTR_SEARCH_LEAF, mtr) + == btr_pcur_t::SAME_ALL) { + const rec_t* after_rec = btr_pcur_get_rec(cascade->pcur); + if (page_rec_is_user_rec(after_rec) + && !rec_get_deleted_flag(after_rec, + dict_table_is_comp(table))) { + ha_innobase* ib = static_cast(child_mysql_table->file); + row_prebuilt_t* prebuilt = ib->innobase_prebuilt(); + if (prebuilt != NULL && prebuilt->mysql_template != NULL) { + MY_BITMAP* old_read_set = child_mysql_table->read_set; + MY_BITMAP* old_write_set = child_mysql_table->write_set; + MY_BITMAP* old_rpl_write_set = child_mysql_table->rpl_write_set; + child_mysql_table->column_bitmaps_set_no_signal( + &child_mysql_table->tmp_set, &child_mysql_table->tmp_set); + bitmap_set_all(&child_mysql_table->tmp_set); + if (Field **vf = child_mysql_table->vfield) { + for (; *vf; vf++) { + bitmap_clear_bit(&child_mysql_table->tmp_set, (*vf)->field_index); + } + } + if (child_mysql_table->rpl_write_set == NULL) { + child_mysql_table->rpl_write_set = &child_mysql_table->tmp_set; + } + ib->rebuild_template_for_cascade_binlog_row_image(); + + mem_heap_t* offs_heap = NULL; + rec_offs offsets_[REC_OFFS_NORMAL_SIZE]; + rec_offs_init(offsets_); + const rec_offs* offsets = rec_get_offsets( + after_rec, clust_index, offsets_, + clust_index->n_core_fields, + ULINT_UNDEFINED, &offs_heap); + dict_index_t* saved_index = prebuilt->index; + prebuilt->index = clust_index; + have_after_image = row_sel_store_mysql_rec(after_mysql_rec, prebuilt, + after_rec, NULL, true, + clust_index, offsets); + prebuilt->index = saved_index; + child_mysql_table->column_bitmaps_set_no_signal(old_read_set, old_write_set); + child_mysql_table->rpl_write_set = old_rpl_write_set; + ib->reset_template_for_cascade_binlog_row_image(); + if (UNIV_LIKELY_NULL(offs_heap)) { + mem_heap_free(offs_heap); + } + } + } + } + } + /* Restore pcur position */ if (pcur->restore_position(BTR_SEARCH_LEAF, mtr) - != btr_pcur_t::SAME_ALL) { + != btr_pcur_t::SAME_ALL && err == DB_SUCCESS) { + err = DB_CORRUPTION; + } + + mtr_commit(mtr); + + if (can_cascade_binlog + && (cascade->is_delete == PLAIN_DELETE || have_after_image)) { + /* + Queue the cascade row event (both the DELETE and the UPDATE case) + and flush it later from ha_innobase::flush_pending_cascade_binlog(). + Both kinds go through the same queue so that events are written to + the binary log in the same order the cascade operations were + executed. Logging deletes immediately here would place every cascade + delete ahead of every deferred update within a statement, which + reorders events that touch the same row and can make the replica + apply an update to an already-deleted row. + */ + const bool is_delete = (cascade->is_delete == PLAIN_DELETE); + const ulint len = child_mysql_table->s->reclength; + unsigned char* before_copy = static_cast( + my_malloc(PSI_INSTRUMENT_ME, len, MYF(MY_WME))); + unsigned char* after_copy = NULL; + if (!is_delete) { + after_copy = static_cast( + my_malloc(PSI_INSTRUMENT_ME, len, MYF(MY_WME))); + } + + if (before_copy != NULL && (is_delete || after_copy != NULL)) { + thd_binlog_mark_fk_cascade_events(trx->mysql_thd); + memcpy(before_copy, before_mysql_rec, len); + if (!is_delete) { + memcpy(after_copy, after_mysql_rec, len); + } + + trx_cascade_binlog_row_event ev; + ev.table = child_mysql_table; + ev.before_record = before_copy; + ev.after_record = after_copy; + ev.log_func = reinterpret_cast( + is_delete + ? row_ins_fk_cascade_delete_binlog_row + : row_ins_fk_cascade_update_binlog_row); + trx->pending_cascade_binlog_row_events.push_back(ev); + } else { + if (before_copy != NULL) { + my_free(before_copy); + } + if (after_copy != NULL) { + my_free(after_copy); + } + } + } + + mtr_start(mtr); + if (pcur->restore_position(BTR_SEARCH_LEAF, mtr) + != btr_pcur_t::SAME_ALL && err == DB_SUCCESS) { err = DB_CORRUPTION; } @@ -2564,7 +2856,7 @@ statement @return true if it is insert statement */ static bool thd_sql_is_insert(const THD *thd) noexcept { - switch (thd->lex->sql_command) { + switch (thd_sql_command(thd)) { case SQLCOM_INSERT: case SQLCOM_INSERT_SELECT: return true; diff --git a/storage/innobase/row/row0sel.cc b/storage/innobase/row/row0sel.cc index 6866619b7c121..79cbf06c369dc 100644 --- a/storage/innobase/row/row0sel.cc +++ b/storage/innobase/row/row0sel.cc @@ -3134,7 +3134,7 @@ be needed in the query. @retval true on success @retval false if not all columns could be retrieved */ MY_ATTRIBUTE((warn_unused_result)) -static bool row_sel_store_mysql_rec( +bool row_sel_store_mysql_rec( byte* mysql_rec, row_prebuilt_t* prebuilt, const rec_t* rec, diff --git a/storage/innobase/trx/trx0trx.cc b/storage/innobase/trx/trx0trx.cc index 3409c66ea0796..511e2aa1dc594 100644 --- a/storage/innobase/trx/trx0trx.cc +++ b/storage/innobase/trx/trx0trx.cc @@ -161,6 +161,21 @@ trx_init( #endif /* WITH_WSREP */ } +void trx_t::free_cascade_binlog_row_events() +{ + /* The before/after record images are allocated in + row_ins_foreign_check_on_constraint() and are normally freed when + ha_innobase::flush_pending_cascade_binlog() emits them at commit. + If the transaction rolls back, or the events are never flushed, + the buffers would otherwise leak, so free them here. + Note: my_free(NULL) is a no-op. */ + for (auto& ev : pending_cascade_binlog_row_events) { + my_free(ev.before_record); + my_free(ev.after_record); + } + pending_cascade_binlog_row_events.clear(); +} + /** For managing the life-cycle of the trx_t instance that we get from the pool. */ struct TrxFactory { @@ -179,6 +194,9 @@ struct TrxFactory { new(&trx->mod_tables) trx_mod_tables_t(); + new(&trx->pending_cascade_binlog_row_events) + std::vector(); + new(&trx->lock.table_locks) lock_list(); new(&trx->read_view) ReadView(); @@ -244,6 +262,9 @@ struct TrxFactory { trx->mod_tables.~trx_mod_tables_t(); + trx->free_cascade_binlog_row_events(); + trx->pending_cascade_binlog_row_events.~vector(); + ut_ad(!trx->read_view.is_open()); trx->lock.table_locks.~lock_list(); @@ -410,6 +431,8 @@ void trx_t::free() noexcept trx_sys.deregister_trx(this); check_unique_secondary= true; check_foreigns= true; + + free_cascade_binlog_row_events(); assert_freed(); trx_sys.rw_trx_hash.put_pins(this); mysql_thd= nullptr; @@ -930,7 +953,7 @@ trx_start_low( /* Check whether it is an AUTOCOMMIT SELECT */ if (const THD* thd = trx->mysql_thd) { trx->auto_commit = !(thd->variables.option_bits - & (OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN)) + & (OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN)) && thd->lex->sql_command == SQLCOM_SELECT; trx->read_only = (!trx->dict_operation && thd->tx_read_only) || srv_read_only_mode; From 0ceaa418b58a5735d7ff3943f9679849142ca5c3 Mon Sep 17 00:00:00 2001 From: sjaakola Date: Wed, 15 Jul 2026 14:20:22 +0300 Subject: [PATCH 2/3] MDEV-38243 Write binlog row events for changes done by cascading FK operations Fixes according to Kristian Nielsen's review: * Removed obsolete checks for slave thread * Supporting slave with old MariaDB version. Events logged in cascade operation are additionally flagged with the long-standing NO_FOREIGN_KEY_CHECKS_F, so a replica that does not understand FK_CASCADE_EVENTS_F still disables foreign key checks and does not re-execute the cascade Also, thee are now binlog event flags to mark both original and derived events. This will make it possible for the slave to choose whether to use the derived events in applying or to execute the cascade operation There is a new test rpl.rpl_fk_cascade_binlog_row_old_slave, for checking compatibility with replication slave of old mariadb version --- ...rpl_fk_cascade_binlog_row_old_slave.result | 53 +++++++++ ..._fk_cascade_binlog_row_slave_option.result | 52 +++++++++ .../rpl_fk_cascade_binlog_row_old_slave.test | 84 ++++++++++++++ ...pl_fk_cascade_binlog_row_slave_option.test | 109 ++++++++++++++++++ sql/handler.cc | 2 +- sql/log.cc | 6 +- sql/log_event.h | 15 +++ sql/log_event_server.cc | 10 +- sql/sql_class.cc | 13 ++- sql/sql_class.h | 10 ++ storage/innobase/handler/ha_innodb.cc | 10 ++ storage/innobase/row/row0ins.cc | 8 +- 12 files changed, 362 insertions(+), 10 deletions(-) create mode 100644 mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_old_slave.result create mode 100644 mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_slave_option.result create mode 100644 mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_old_slave.test create mode 100644 mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_slave_option.test diff --git a/mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_old_slave.result b/mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_old_slave.result new file mode 100644 index 0000000000000..6da08373b8d3d --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_old_slave.result @@ -0,0 +1,53 @@ +include/master-slave.inc +[connection master] +connection slave; +include/stop_slave.inc +SET @old_dbug= @@GLOBAL.debug_dbug; +SET @old_exec_mode= @@GLOBAL.slave_exec_mode; +SET GLOBAL slave_exec_mode= STRICT; +SET GLOBAL debug_dbug= "+d,rpl_emulate_old_slave_fk_cascade"; +include/start_slave.inc +connection master; +SET SESSION default_storage_engine=InnoDB; +SET SESSION binlog_format=ROW; +SET SESSION rpl_use_binlog_events_for_fk_cascade=1; +CREATE TABLE p (id INT PRIMARY KEY, v INT) ENGINE=InnoDB; +CREATE TABLE c ( +id INT PRIMARY KEY, pid INT, v INT, KEY(pid), +CONSTRAINT fk_c FOREIGN KEY (pid) REFERENCES p(id) +ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB; +CREATE TABLE cn ( +id INT PRIMARY KEY, pid INT, v INT, KEY(pid), +CONSTRAINT fk_cn FOREIGN KEY (pid) REFERENCES p(id) +ON DELETE SET NULL ON UPDATE SET NULL +) ENGINE=InnoDB; +INSERT INTO p VALUES (1,10),(2,20),(3,30); +INSERT INTO c VALUES (100,1,1000),(200,2,2000),(300,3,3000); +INSERT INTO cn VALUES (100,1,1000),(200,2,2000),(300,3,3000); +UPDATE p SET id=11 WHERE id=1; +DELETE FROM p WHERE id=2; +connection slave; +connection slave; +# Last_SQL_Errno = 0 (expected 0) +SELECT * FROM p ORDER BY id; +id v +3 30 +11 10 +SELECT * FROM c ORDER BY id; +id pid v +100 11 1000 +300 3 3000 +SELECT * FROM cn ORDER BY id; +id pid v +100 NULL 1000 +200 NULL 2000 +300 3 3000 +connection slave; +include/stop_slave.inc +SET GLOBAL debug_dbug= @old_dbug; +SET GLOBAL slave_exec_mode= @old_exec_mode; +include/start_slave.inc +connection master; +DROP TABLE c, cn, p; +include/rpl_end.inc diff --git a/mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_slave_option.result b/mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_slave_option.result new file mode 100644 index 0000000000000..9fa0419649b6e --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_fk_cascade_binlog_row_slave_option.result @@ -0,0 +1,52 @@ +include/master-slave.inc +[connection master] +connection master; +SET SESSION default_storage_engine=InnoDB; +SET SESSION binlog_format=ROW; +SELECT @@session.rpl_use_binlog_events_for_fk_cascade; +@@session.rpl_use_binlog_events_for_fk_cascade +0 +CREATE TABLE p ( +id INT PRIMARY KEY, +v INT +) ENGINE=InnoDB; +CREATE TABLE c ( +id INT PRIMARY KEY, +pid INT, +v INT, +KEY(pid), +CONSTRAINT fk FOREIGN KEY (pid) REFERENCES p(id) +ON DELETE CASCADE +ON UPDATE CASCADE +) ENGINE=InnoDB; +INSERT INTO p VALUES (1, 10); +INSERT INTO c VALUES (100, 1, 20); +connection slave; +connection slave; +include/stop_slave.inc +SET GLOBAL rpl_use_binlog_events_for_fk_cascade=1; +include/start_slave.inc +connection master; +UPDATE p SET id=2 WHERE id=1; +DELETE FROM p WHERE id=2; +connection slave; +connection slave; +SELECT COUNT(*) FROM p; +COUNT(*) +0 +SELECT COUNT(*) FROM c; +COUNT(*) +0 +FLUSH BINARY LOGS; +FOUND 1 /### DELETE FROM `test`.`p`/ in fk_cascade_slave_option.sql +NOT FOUND /### DELETE FROM `test`.`c`/ in fk_cascade_slave_option.sql +NOT FOUND /### UPDATE `test`.`c`/ in fk_cascade_slave_option.sql +connection slave; +include/stop_slave.inc +SET GLOBAL rpl_use_binlog_events_for_fk_cascade=0; +include/start_slave.inc +connection master; +DROP TABLE c, p; +connection slave; +connection master; +include/rpl_end.inc diff --git a/mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_old_slave.test b/mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_old_slave.test new file mode 100644 index 0000000000000..cd3a0af6e476d --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_old_slave.test @@ -0,0 +1,84 @@ +--source include/have_innodb.inc +--source include/have_log_bin.inc +--source include/have_binlog_format_row.inc +# The old-replica emulation relies on a DBUG_EXECUTE_IF hook, so a debug build +# is required. +--source include/have_debug.inc + +--source include/master-slave.inc + +# +# MDEV-38243 / FR-COMPAT-1: a cascade-logged transaction produced by a new +# origin (rpl_use_binlog_events_for_fk_cascade=ON) must apply correctly on a +# replica running an OLDER MariaDB version that does not understand +# FK_CASCADE_EVENTS_F, WITHOUT any apply error, in strict slave_exec_mode. +# +# Such an old replica is emulated here with the debug keyword +# rpl_emulate_old_slave_fk_cascade, which forces the applier to ignore +# FK_CASCADE_EVENTS_F and rely solely on the long-standing +# NO_FOREIGN_KEY_CHECKS_F flag - exactly a pre-MDEV-38243 server would. +# The compatibility therefore holds only because the origin also stamps +# NO_FOREIGN_KEY_CHECKS_F on the cascade-logged events. +# +# NOTE: without the DBUG hook this test would still pass on a new build (the +# new applier disables FK checks via FK_CASCADE_EVENTS_F), but would not be +# exercising the old-replica path. It is meaningful only with the hook present. +# + +# --- Emulate an old, strict-mode replica ------------------------------------ +connection slave; +--source include/stop_slave.inc +SET @old_dbug= @@GLOBAL.debug_dbug; +SET @old_exec_mode= @@GLOBAL.slave_exec_mode; +SET GLOBAL slave_exec_mode= STRICT; +SET GLOBAL debug_dbug= "+d,rpl_emulate_old_slave_fk_cascade"; +--source include/start_slave.inc + +# --- Origin: feature ON ------------------------------------------------------ +connection master; +SET SESSION default_storage_engine=InnoDB; +SET SESSION binlog_format=ROW; +SET SESSION rpl_use_binlog_events_for_fk_cascade=1; + +CREATE TABLE p (id INT PRIMARY KEY, v INT) ENGINE=InnoDB; +CREATE TABLE c ( + id INT PRIMARY KEY, pid INT, v INT, KEY(pid), + CONSTRAINT fk_c FOREIGN KEY (pid) REFERENCES p(id) + ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB; +CREATE TABLE cn ( + id INT PRIMARY KEY, pid INT, v INT, KEY(pid), + CONSTRAINT fk_cn FOREIGN KEY (pid) REFERENCES p(id) + ON DELETE SET NULL ON UPDATE SET NULL +) ENGINE=InnoDB; + +INSERT INTO p VALUES (1,10),(2,20),(3,30); +INSERT INTO c VALUES (100,1,1000),(200,2,2000),(300,3,3000); +INSERT INTO cn VALUES (100,1,1000),(200,2,2000),(300,3,3000); + +# ON UPDATE CASCADE (c.pid 1->11) and ON UPDATE SET NULL (cn.pid 1->NULL) +UPDATE p SET id=11 WHERE id=1; +# ON DELETE CASCADE (c row 200 gone) and ON DELETE SET NULL (cn.pid 2->NULL) +DELETE FROM p WHERE id=2; + +# --- The emulated old replica must apply without error ---------------------- +# sync_slave_with_master fails the test if the SQL thread has stopped on error. +sync_slave_with_master; + +connection slave; +--let $errno= query_get_value(SHOW SLAVE STATUS, Last_SQL_Errno, 1) +--echo # Last_SQL_Errno = $errno (expected 0) +SELECT * FROM p ORDER BY id; +SELECT * FROM c ORDER BY id; +SELECT * FROM cn ORDER BY id; + +# --- Cleanup ---------------------------------------------------------------- +connection slave; +--source include/stop_slave.inc +SET GLOBAL debug_dbug= @old_dbug; +SET GLOBAL slave_exec_mode= @old_exec_mode; +--source include/start_slave.inc + +connection master; +DROP TABLE c, cn, p; +--source include/rpl_end.inc diff --git a/mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_slave_option.test b/mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_slave_option.test new file mode 100644 index 0000000000000..831394ab52d93 --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_fk_cascade_binlog_row_slave_option.test @@ -0,0 +1,109 @@ +--source include/have_innodb.inc +--source include/have_log_bin.inc +--source include/have_binlog_format_row.inc + +--source include/master-slave.inc + +# +# MDEV-38243: rpl_use_binlog_events_for_fk_cascade enabled only on the slave. +# +# The master runs with the feature OFF (default), so it binlogs only the +# parent-table row events, not the cascaded child row events. The slave has +# the feature ON. Because the row-based applier does not prelock the FK-child +# tables, find_fk_open_table() finds no open child table on the applier, so no +# cascade row events are captured there. The slave applies the cascade the +# traditional way (InnoDB re-executes it) and its own binary log contains only +# the parent events. +# +# This test pins that behaviour: enabling the option on the slave is currently +# a no-op for the applier path, while replication stays correct. It exists so +# that a future change which makes the option effective on the applier (by +# opening the child table there) will visibly change this recorded result. +# + +connection master; +SET SESSION default_storage_engine=InnoDB; +SET SESSION binlog_format=ROW; + +# The master keeps the feature OFF (the default). +SELECT @@session.rpl_use_binlog_events_for_fk_cascade; + +CREATE TABLE p ( + id INT PRIMARY KEY, + v INT +) ENGINE=InnoDB; + +CREATE TABLE c ( + id INT PRIMARY KEY, + pid INT, + v INT, + KEY(pid), + CONSTRAINT fk FOREIGN KEY (pid) REFERENCES p(id) + ON DELETE CASCADE + ON UPDATE CASCADE +) ENGINE=InnoDB; + +INSERT INTO p VALUES (1, 10); +INSERT INTO c VALUES (100, 1, 20); + +sync_slave_with_master; + +# Turn the feature ON only on the slave, and restart the SQL thread so the +# applier THD picks up the new global value. +connection slave; +--source include/stop_slave.inc +SET GLOBAL rpl_use_binlog_events_for_fk_cascade=1; +--source include/start_slave.inc + +connection master; +UPDATE p SET id=2 WHERE id=1; +DELETE FROM p WHERE id=2; + +sync_slave_with_master; + +connection slave; +SELECT COUNT(*) FROM p; +SELECT COUNT(*) FROM c; + +# Inspect the slave's own binary log. It must contain the parent events but +# NOT explicit cascade row events for the child table, because the applier +# does not capture cascades (the child table is not prelocked on the applier). +--let $binlog = query_get_value(SHOW MASTER STATUS, File, 1) +--let $datadir = `SELECT @@datadir` + +FLUSH BINARY LOGS; + +--exec $MYSQL_BINLOG --verbose --verbose --base64-output=DECODE-ROWS $datadir/$binlog > $MYSQLTEST_VARDIR/tmp/fk_cascade_slave_option.sql + +# Parent DELETE is present in the slave's binary log. +--let SEARCH_PATTERN= ### DELETE FROM `test`.`p` +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/fk_cascade_slave_option.sql +--let SEARCH_ABORT= NOT FOUND +--source include/search_pattern_in_file.inc + +# No explicit cascade child row events: the option is a no-op on the applier. +--let SEARCH_PATTERN= ### DELETE FROM `test`.`c` +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/fk_cascade_slave_option.sql +--let SEARCH_ABORT= FOUND +--source include/search_pattern_in_file.inc + +--let SEARCH_PATTERN= ### UPDATE `test`.`c` +--let SEARCH_FILE= $MYSQLTEST_VARDIR/tmp/fk_cascade_slave_option.sql +--let SEARCH_ABORT= FOUND +--source include/search_pattern_in_file.inc + +# Cleanup: restore the slave option and drop the tables. +connection slave; +--source include/stop_slave.inc +SET GLOBAL rpl_use_binlog_events_for_fk_cascade=0; +--source include/start_slave.inc + +connection master; +DROP TABLE c, p; + +sync_slave_with_master; + +connection master; +--remove_file $MYSQLTEST_VARDIR/tmp/fk_cascade_slave_option.sql + +--source include/rpl_end.inc diff --git a/sql/handler.cc b/sql/handler.cc index a7d837a5b7049..110c1de3109d6 100644 --- a/sql/handler.cc +++ b/sql/handler.cc @@ -120,7 +120,7 @@ KEY_CREATE_INFO default_key_create_info= static void flush_pending_cascade_binlog_for_thd(THD *thd) { - if (!thd || thd->rgi_slave) return; + if (!thd) return; TABLE *table; for (table = thd->open_tables; table; table = table->next) { diff --git a/sql/log.cc b/sql/log.cc index 2c155fda778b7..b90c89972e272 100644 --- a/sql/log.cc +++ b/sql/log.cc @@ -8069,7 +8069,6 @@ int binlog_flush_pending_rows_event(THD *thd, bool stmt_end, if (stmt_end) { if (thd->binlog_fk_cascade_events && - !thd->rgi_slave && (thd->variables.rpl_use_binlog_events_for_fk_cascade || WSREP_EMULATE_BINLOG(thd))) { @@ -8316,7 +8315,10 @@ Event_log::prepare_pending_rows_event(THD *thd, TABLE* table, ev->server_id= serv_id; // I don't like this, it's too easy to forget. if (thd->binlog_fk_cascade_events) - ev->set_flags(Rows_log_event::FK_CASCADE_EVENTS_F); + ev->set_flags(Rows_log_event::FK_CASCADE_EVENTS_F | + Rows_log_event::NO_FOREIGN_KEY_CHECKS_F); + if (thd->binlog_fk_cascade_derived) + ev->set_flags(Rows_log_event::FK_CASCADE_DERIVED_F); /* flush the pending event and replace it with the newly created event... diff --git a/sql/log_event.h b/sql/log_event.h index c794b35e7ee04..9be68369ac8c6 100644 --- a/sql/log_event.h +++ b/sql/log_event.h @@ -4792,8 +4792,23 @@ class Rows_log_event : public Log_event */ COMPLETE_ROWS_F = (1U << 3), + /* + Set on every row event that belongs to a statement whose FK cascade + changes were logged as explicit row events (originating rows and cascade + derived rows alike). The applier uses it to suppress re-cascading. + */ FK_CASCADE_EVENTS_F = (1U << 4), + /* + Set only on the cascade-derived row events (the child-row changes that + the master produced by executing the FK cascade), and NOT on the + originating statement's own row events. Combined with + FK_CASCADE_EVENTS_F it lets the applier tell "root" rows from + "cascade-derived" rows, e.g. to optionally re-execute the cascade + instead of applying the derived events. + */ + FK_CASCADE_DERIVED_F = (1U << 5), + /* Value of the OPTION_NO_CHECK_CONSTRAINT_CHECKS flag in thd->options */ NO_CHECK_CONSTRAINT_CHECKS_F = (1U << 7) }; diff --git a/sql/log_event_server.cc b/sql/log_event_server.cc index 9e64cf5eb2f64..6966711976135 100644 --- a/sql/log_event_server.cc +++ b/sql/log_event_server.cc @@ -5042,7 +5042,15 @@ int Rows_log_event::do_apply_event(rpl_group_info *rgi) not re-run the cascade, so foreign key checks are disabled for these events just as they are for NO_FOREIGN_KEY_CHECKS_F. */ - if (get_flags(NO_FOREIGN_KEY_CHECKS_F) || get_flags(FK_CASCADE_EVENTS_F)) + bool fk_cascade_events= get_flags(FK_CASCADE_EVENTS_F); + /* + emulate a pre-MDEV-38243 replica that does not understand + FK_CASCADE_EVENTS_F, so that the decision below relies solely on the + long-standing NO_FOREIGN_KEY_CHECKS_F. Used by the cross-version + compatibility test rpl.rpl_fk_cascade_binlog_row_old_slave. + */ + DBUG_EXECUTE_IF("rpl_emulate_old_slave_fk_cascade", fk_cascade_events= false;); + if (get_flags(NO_FOREIGN_KEY_CHECKS_F) || fk_cascade_events) thd->variables.option_bits|= OPTION_NO_FOREIGN_KEY_CHECKS; else thd->variables.option_bits&= ~OPTION_NO_FOREIGN_KEY_CHECKS; diff --git a/sql/sql_class.cc b/sql/sql_class.cc index b833e0de3e45b..d81da48861c77 100644 --- a/sql/sql_class.cc +++ b/sql/sql_class.cc @@ -99,13 +99,21 @@ void THD::binlog_mark_fk_cascade_events() if (binlog_cache_mngr *cache_mngr= binlog_get_cache_mngr()) { + /* + Also set the NO_FOREIGN_KEY_CHECKS_F. An older slave does + not understand FK_CASCADE_EVENTS_F, but it does understand + NO_FOREIGN_KEY_CHECKS_F, so setting it makes such a slave disable foreign + key checks for these events. + */ if (Rows_log_event *pending= binlog_get_pending_rows_event( cache_mngr, use_trans_cache(this, false))) - pending->set_flags(Rows_log_event::FK_CASCADE_EVENTS_F); + pending->set_flags(Rows_log_event::FK_CASCADE_EVENTS_F | + Rows_log_event::NO_FOREIGN_KEY_CHECKS_F); if (Rows_log_event *pending= binlog_get_pending_rows_event( cache_mngr, use_trans_cache(this, true))) - pending->set_flags(Rows_log_event::FK_CASCADE_EVENTS_F); + pending->set_flags(Rows_log_event::FK_CASCADE_EVENTS_F | + Rows_log_event::NO_FOREIGN_KEY_CHECKS_F); } } @@ -960,6 +968,7 @@ THD::THD(my_thread_id id, bool is_wsrep_applier) binlog_evt_union.do_union= FALSE; binlog_table_maps= FALSE; binlog_fk_cascade_events= FALSE; + binlog_fk_cascade_derived= FALSE; binlog_xid= 0; enable_slow_log= 0; durability_property= HA_REGULAR_DURABILITY; diff --git a/sql/sql_class.h b/sql/sql_class.h index f768b7444ed92..a7789b0a7d9c3 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -3856,7 +3856,16 @@ class THD: public THD_count, /* this must be first */ bool binlog_fk_cascade_events; + /* + True only while the queued FK-cascade row events are being flushed into + the binlog cache, so that the events created during that flush are marked + as cascade-derived (FK_CASCADE_DERIVED_F). + */ + bool binlog_fk_cascade_derived; + void binlog_mark_fk_cascade_events(); + void binlog_begin_fk_cascade_derived() { binlog_fk_cascade_derived= true; } + void binlog_end_fk_cascade_derived() { binlog_fk_cascade_derived= false; } void issue_unsafe_warnings(); void reset_unsafe_warnings() @@ -3866,6 +3875,7 @@ class THD: public THD_count, /* this must be first */ { binlog_table_maps= 0; binlog_fk_cascade_events= false; + binlog_fk_cascade_derived= false; } bool binlog_table_should_be_logged(const LEX_CSTRING *db); diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc index 4c373b5ac2baf..eb98fe740bd4f 100644 --- a/storage/innobase/handler/ha_innodb.cc +++ b/storage/innobase/handler/ha_innodb.cc @@ -8897,6 +8897,14 @@ ha_innobase::flush_pending_cascade_binlog() m_user_thd->binlog_mark_fk_cascade_events(); + /* + Everything logged from here until the end of the loop is a + cascade-derived row event; mark it so that it is distinguishable from + the originating statement's own row events (which were already logged + above with only FK_CASCADE_EVENTS_F). + */ + m_user_thd->binlog_begin_fk_cascade_derived(); + for (auto& ev : trx->pending_cascade_binlog_row_events) { if (ev.table == NULL || ev.table->file == NULL) { if (ev.before_record) { @@ -8940,6 +8948,8 @@ ha_innobase::flush_pending_cascade_binlog() my_free(ev.after_record); } + m_user_thd->binlog_end_fk_cascade_derived(); + trx->pending_cascade_binlog_row_events.clear(); } diff --git a/storage/innobase/row/row0ins.cc b/storage/innobase/row/row0ins.cc index bfdec70780b63..7cfe57f2feaf9 100644 --- a/storage/innobase/row/row0ins.cc +++ b/storage/innobase/row/row0ins.cc @@ -62,8 +62,6 @@ Created 4/20/1996 Heikki Tuuri TABLE *find_fk_open_table(THD *thd, const char *db, size_t db_len, const char *table, size_t table_len); -extern "C" bool thd_is_slave(const MYSQL_THD thd); - static bool row_ins_fk_cascade_delete_binlog_row(THD *thd, TABLE *table, Event_log *bin_log, binlog_cache_data *cache_data, @@ -1441,10 +1439,12 @@ row_ins_foreign_check_on_constraint( cascade->state = UPD_NODE_UPDATE_CLUSTERED; /* - Don't build cascade binlog events on a slave thread, + Capture cascade row events whenever the session requests it. + If the master has binlogged the cascaded rows, the events carry + FK_CASCADE_EVENTS_F, causing the applier to run with foreign key + checks disabled, and this cascade function is never reached. */ if (trx->mysql_thd && - !thd_is_slave(trx->mysql_thd) && thd_rpl_use_binlog_events_for_fk_cascade(trx->mysql_thd)) { child_mysql_table = row_ins_find_open_table_for_cascade_binlog(trx, table); From fbda756d4743fe8ad0221e4de08ef638178935d1 Mon Sep 17 00:00:00 2001 From: sjaakola Date: Mon, 10 Aug 2026 12:20:49 +0300 Subject: [PATCH 3/3] MDEV-38243 Write binlog row events for changes done by cascading FK operations Refactoring according to Serg's review. In this version, SE/server API now narrows the SE role to just report the changes done by foreign key cascading, and server side does most of the work after that. Added a design document MDEV-38243-design.md --- MDEV-38243-design.md | 355 ++++++++++++++++++++++++++ include/mysql/service_thd_binlog.h | 32 +-- sql/handler.cc | 28 +- sql/handler.h | 8 - sql/log.cc | 9 +- sql/sql_class.cc | 144 +++++++++-- sql/sql_class.h | 24 ++ sql/sql_parse.cc | 17 ++ storage/innobase/handler/ha_innodb.cc | 92 ------- storage/innobase/handler/ha_innodb.h | 4 - storage/innobase/include/trx0trx.h | 13 - storage/innobase/row/row0ins.cc | 83 ++---- storage/innobase/trx/trx0trx.cc | 22 -- 13 files changed, 560 insertions(+), 271 deletions(-) create mode 100644 MDEV-38243-design.md diff --git a/MDEV-38243-design.md b/MDEV-38243-design.md new file mode 100644 index 0000000000000..0deb2757d98dd --- /dev/null +++ b/MDEV-38243-design.md @@ -0,0 +1,355 @@ +# MDEV-38243 — Binlog row events for cascading foreign key operations + +## Design document + +Status: implemented on branch `MDEV-38243` + +--- + +## 1. Problem statement + +Foreign key cascade actions — `ON DELETE CASCADE`, `ON UPDATE CASCADE`, +`ON DELETE SET NULL`, `ON UPDATE SET NULL` — are executed **inside InnoDB** +(`row_ins_foreign_check_on_constraint()`), below the SQL layer. In row-based +replication the SQL layer only logs the row changes it drives directly, i.e. +the change to the *parent* table named in the statement. The cascaded changes +to *child* tables are performed by InnoDB and never surface to the binlogging +layer. + +Currently replication handles changes done by the foreing key constraint cascade +execution so that the replication **SQL slave re-executes the cascade**: +it applies the parent-table row event with foreign key checks enabled, and its +own InnoDB reproduces the child-row changes. + +Re-executing the cascade on the replica is a source of problems: + +- **Non-determinism / divergence** when the replica's schema, indexes or FK + definitions differ, or when `SET NULL` ordering is ambiguous. +- **Parallel-apply hazards**, particularly for Galera appliers, where a cascade + fired during apply interacts unpredictably with other concurrent appliers. + Conflicts in galera applying is the primary reason the feature. + +## 2. Prior Art +MySQL 9.6 has refactored Foreign key constraint handling to happen in server side, +which directly allows recording binlog events for cascading operation changes. +See: https://blogs.oracle.com/mysql/no-more-hidden-changes-how-mysql-9-6-transforms-foreign-key-management + +If similar feature is planned to be implemented in Mariadb, then this MDEV-28243 +becomes obsolete. + +## 3. Goal + +Have the **origin** capture the row changes produced by cascading FK operations +and write them into the binary log as **explicit row events**, so that the +**replica applies just those events and does not re-run the cascade**. This +makes applying deterministic and free of cascade-induced parallel-apply hazards. + +## 4. Feature switch + +- New session variable **`rpl_use_binlog_events_for_fk_cascade`**, default + `OFF`. It cannot be changed inside a transaction or sub-statement + (`error_if_in_trans_or_substatement`). +- The same capture path is taken automatically under Galera when binloggiing is + off, code paths with: **`WSREP_EMULATE_BINLOG`** . +- Row-based binary logging is required; the capture is a no-op otherwise. + +## 5. Architecture / data flow + +``` + Statement executes on origin + │ + ▼ + InnoDB parent DML ── triggers ──► row_ins_foreign_check_on_constraint() + │ │ (feature ON, ROW format) + │ ▼ + │ capture BEFORE image of child row + │ run the real cascade (row_update_cascade_for_mysql) + │ capture AFTER image (UPDATE / SET NULL only) + │ report it: thd_binlog_cascade_{delete,update}_row() + │ │ + │ ▼ (server side, sql/sql_class.cc) + │ THD::binlog_report_cascade_row() copies the + │ record images and queues them on the THD + ▼ + parent row event logged (flagged) + │ + ▼ + statement end / commit ──► THD::flush_pending_cascade_binlog() + │ emits queued child events in order + ▼ + binary log: [parent event][derived child events...] + all flagged, applied verbatim on replica +``` + +The engine/server boundary sits at the *report* step: the engine says only +"this child row changed, here are its before/after images"; every decision +about whether, when and how to binlog it is made on the server side. See §6.1. + +## 6. Origin side — capture + +Location: `storage/innobase/row/row0ins.cc`, +`row_ins_foreign_check_on_constraint()`. + +When the feature is engaged (`thd_rpl_use_binlog_events_for_fk_cascade()`), and +the cascade child table is available as an open MySQL `TABLE`: + +1. **Locate the child table.** `row_ins_find_open_table_for_cascade_binlog()` + parses the child's db/table name and calls `find_fk_open_table()`, which + returns the child `TABLE` only if it was opened via FK **prelocking** + (`TABLE_LIST::PRELOCK_FK`). See §10 for the consequence on the replica. + +2. **Eligibility guard.** `row_ins_allow_fk_cascade_binlog_for_table()` rejects + a table that has *no primary key* **and** a *virtual column participating in + a key* (an unsafe row image); otherwise it is allowed. + +3. **Capture the before-image.** The handler's column read/write bitmaps and + `rpl_write_set` are temporarily switched to an all-columns set (virtual + columns cleared), the InnoDB row template is rebuilt for a full row image + (`ha_innobase::rebuild_template_for_cascade_binlog_row_image()` → + `reset_template()` + `build_template(true)`), and the current child record is + materialised into a MySQL-format buffer with `row_sel_store_mysql_rec()`. + Bitmaps/template are then restored. + +4. **Run the actual cascade** via `row_update_cascade_for_mysql()`. + +5. **Capture the after-image** (for `UPDATE` / `SET NULL`; not for + `PLAIN_DELETE`) by re-positioning the cursor to the just-modified record and + materialising it the same way. + +6. **Report the change to the server.** The engine calls + `thd_binlog_cascade_delete_row(thd, table, before_rec)` or + `thd_binlog_cascade_update_row(thd, table, before_rec, after_rec)` and is + then done with the row — the server copies the images, so the temp-heap + buffers may be reused or freed immediately afterwards. + +### 6.1 The SE ↔ server interface + +`include/mysql/service_thd_binlog.h` exposes exactly two cascade entry points: + +```c +void thd_binlog_cascade_delete_row(MYSQL_THD thd, struct TABLE *table, + const unsigned char *before_record); +void thd_binlog_cascade_update_row(MYSQL_THD thd, struct TABLE *table, + const unsigned char *before_record, + const unsigned char *after_record); +``` + +The engine passes only the child `TABLE` and the affected row image(s) in +MySQL record format. It does **not** see `Event_log`, `binlog_cache_data`, +`enum_binlog_row_image`, the transactional-cache flag, or the row-logging +function — all of which the earlier revision of this design required it to +supply. Consequently the *policy* — whether the change is binlogged at all, +which log function applies, event ordering, event flagging, and the +commit/rollback lifecycle — lives entirely in `THD` +(`THD::binlog_report_cascade_row()` and the flush/discard pair below). + +This keeps the contract engine-agnostic: any engine that performs FK cascades +internally can adopt it by calling these two functions, without replicating +binlog internals or tracking a queue of its own. + +### Deferred, ordered logging + +Events are **queued, not emitted inline**. The queue preserves *execution +order*. Emitting deletes inline would place every cascade delete ahead of every +deferred update within a statement, reordering events that touch the same row +and potentially making the replica apply an update to an already-deleted row. + +## 7. Lifecycle — flush and discard + +The queue itself is `THD::pending_cascade_binlog_row_events`, a +`Dynamic_array` holding the child `TABLE`, the +server's own copies of the before/after images, and an `is_delete` flag. The +`THD` owns it outright; no engine structure participates in its lifetime. + +**Flush** (`THD::flush_pending_cascade_binlog()`): emits each queued event via +`handler::binlog_log_row()` using an all-columns bitmap, choosing +`Delete_rows_log_event` vs. `Update_rows_log_event` from `is_delete`, and frees +the record copies as it goes. It is driven from two choke points: + +- `binlog_flush_pending_rows_event()` in `sql/log.cc` at **statement end**; +- `ha_commit_trans()` in `sql/handler.cc` at **commit**. + +Temporary tables and tables without a usable `TABLE`/handler are skipped (their +buffers freed). + +**Discard** (`THD::discard_pending_cascade_binlog()`): frees the queued record +copies and empties the array without writing anything. It runs on: + +- `ha_rollback_trans()` — full/statement rollback (`sql/handler.cc`); +- `ha_rollback_to_savepoint()` — rollback to savepoint; +- `~THD()` — backstop only, so that an undrained queue cannot leak. + +Discard is **unconditional** with respect to thread type — it must run for +applier threads too, since it only frees memory. + +### 7.1 Queue lifetime vs. `TABLE` lifetime + +A queued entry stores a raw `TABLE *`. Because the queue now outlives the +engine transaction (it is reclaimed by the server, not by `trx_t::free()`), +the invariant that matters is that the queue is **drained before the statement +closes its tables**. It holds: in `mysql_execute_command()`'s `finish:` block, +`trans_commit_stmt()` / `trans_rollback_stmt()` run *before* +`close_thread_tables_for_query()`; and for a multi-statement transaction the +`in_active_multi_stmt_transaction()` arm of the `ha_commit_trans()` gate drains +per statement, likewise with tables still open. + +This is asserted rather than assumed — `mysql_execute_command()` carries a +`DBUG_ASSERT` immediately after `close_thread_tables_for_query()` that the +queue is empty at the top level (a substatement may legitimately leave rows +queued for the enclosing statement, since the draining commit/rollback is +itself under `! thd->in_sub_stmt`). The assert exists because the +`!table || !table->file` guard inside the flush loop can only catch a *closed* +table, not one that was freed and had its memory reused; a leftover entry would +otherwise be flushed by a later statement against a dangling pointer, silently. + +## 8. Event marking + +Three flags on `Rows_log_event` participate (`sql/log_event.h`): + +| Flag | Bit | Set on | Meaning | +|------|-----|--------|---------| +| `NO_FOREIGN_KEY_CHECKS_F` | 1 | every cascade-logged event | pre-existing flag; disables FK checks on apply | +| `FK_CASCADE_EVENTS_F` | 4 | every cascade-logged event (root + derived) | this statement's cascade rows were logged; suppress re-cascade | +| `FK_CASCADE_DERIVED_F` | 5 | derived (child) events only | distinguishes cascade-derived rows from the originating rows | + +Origin-side mechanism: + +- `THD::binlog_fk_cascade_events` (bool) is set by + `THD::binlog_mark_fk_cascade_events()` on the first cascade of a statement; it + also stamps the currently-pending row events. It is reset in + `reset_binlog_for_next_statement()`. +- `Event_log::prepare_pending_rows_event()` stamps every newly created event + with `FK_CASCADE_EVENTS_F | NO_FOREIGN_KEY_CHECKS_F` while that THD flag is on. +- `THD::binlog_fk_cascade_derived` (bool) is turned on only around the flush + loop (`binlog_begin_fk_cascade_derived()` / `binlog_end_fk_cascade_derived()` + in `THD::flush_pending_cascade_binlog()`), so events created during + the flush additionally get `FK_CASCADE_DERIVED_F`. The originating (parent) + event is created *outside* the flush loop and therefore stays underived. + +Result: the **root** event carries `{EVENTS_F, NO_FK_CHECKS_F}`; each **derived** +event carries `{EVENTS_F, DERIVED_F, NO_FK_CHECKS_F}`. + +## 9. Replica side — apply + +`Rows_log_event::do_apply_event()` (`sql/log_event_server.cc`): if the event +carries `NO_FOREIGN_KEY_CHECKS_F` **or** `FK_CASCADE_EVENTS_F`, it sets +`OPTION_NO_FOREIGN_KEY_CHECKS`, which InnoDB maps to `trx->check_foreigns = +false` (`ha_innodb.cc`). With FK checks off, `row_ins_check_foreign_constraint()` +returns early and `row_ins_foreign_check_on_constraint()` is never reached — the +replica does **not** re-run the cascade. It applies the explicit parent and +child row events directly, in log order. + +No slave-thread guards (`!thd->rgi_slave` / `!thd_is_slave`) are used on the +capture or flush paths. They were removed as redundant: the flag-driven +suppression above already prevents an applier from re-cascading, so capture is +unreachable there for cascade-logged transactions. + +## 10. Backward compatibility (older replica, strict mode) + +An older MariaDB replica does not understand `FK_CASCADE_EVENTS_F`/ +`FK_CASCADE_DERIVED_F` and would ignore them. It **does** understand the ancient +`NO_FOREIGN_KEY_CHECKS_F`. Because the origin sets that flag on every +cascade-logged event, an old replica: + +1. applies the parent event with FK checks disabled → does **not** re-cascade; +2. applies the derived child events as the sole source of child changes. + +No collision occurs and the data is correct **even under strict +`slave_exec_mode`**. Had `NO_FOREIGN_KEY_CHECKS_F` not been set, an old replica +would re-cascade *and* apply the derived events, colliding on the same keys → +`HA_ERR_KEY_NOT_FOUND` and a stopped replica in strict mode (tolerated only in +idempotent mode). + +A newer replica that wants to re-execute the cascade instead of applying the +derived events can still recognise these events via the new flags and override +`NO_FOREIGN_KEY_CHECKS_F` (see §11). + +### 10.1 Old-replica emulation for testing + +Standard mtr runs every server from one build, so a genuine "old binary as +replica" test cannot run in the default suite. To still exercise the mechanism +in-tree, a debug-only injection point emulates a pre-MDEV-38243 replica: + +- `Rows_log_event::do_apply_event()` computes a local `fk_cascade_events` + (initialised from `get_flags(FK_CASCADE_EVENTS_F)`), and + `DBUG_EXECUTE_IF("rpl_emulate_old_slave_fk_cascade", fk_cascade_events= false)` + forces it off. The FK-check decision then depends **only** on + `NO_FOREIGN_KEY_CHECKS_F` — exactly how an old server behaves. + +The test `rpl_fk_cascade_binlog_row_old_slave` enables this keyword on a +strict-mode replica and confirms that a feature-ON origin's `CASCADE` and +`SET NULL` transactions apply with `Last_SQL_Errno=0` and correct data. Because +the emulated replica ignores `FK_CASCADE_EVENTS_F`, a passing run proves the +`NO_FOREIGN_KEY_CHECKS_F` stamp alone suffices; equally, if that stamp were ever +dropped, this test would fail (the emulated replica would re-cascade and +collide). + +## 11. Known limitations and edge cases + +- **Applier capture is a no-op (by design/limitation).** The capture requires + the child table to be open via FK prelocking (`PRELOCK_FK`). A row-based + applier opens only the tables named in the events, not prelocked FK children, + so `find_fk_open_table()` returns NULL on an applier. Therefore, with the + feature enabled **only on a replica** (origin OFF), the replica re-cascades + the classic way and its own binary log contains only parent events — the + option is effectively inert on the applier path. This is pinned by + `rpl_fk_cascade_binlog_row_slave_option`. +- **Row format only.** Statement-based logging is unaffected. +- **Table eligibility.** Tables with no PK and a virtual column in a key are + skipped (§5.2). +- **Observability.** `mysqlbinlog` prints only `STMT_END_F` in its verbose + header; the FK cascade flags are not surfaced there. They are observable only + through apply behaviour (or a future print extension). + +## 12. Future work — "applier decides" + +The `FK_CASCADE_DERIVED_F` marking is groundwork for an optional mode letting an +applier choose between applying the derived events (default) and re-executing +the cascade. Sketch: + +- Add a replica variable, e.g. `slave_fk_cascade_mode = {APPLY_EVENTS | + EXECUTE_CASCADE}`. +- `APPLY_EVENTS` (default): current behaviour. +- `EXECUTE_CASCADE`: for events carrying `FK_CASCADE_EVENTS_F`, do **not** honour + `NO_FOREIGN_KEY_CHECKS_F` on the *root* events (those without + `FK_CASCADE_DERIVED_F`), so InnoDB re-cascades; and **skip** the events + carrying `FK_CASCADE_DERIVED_F`. + +This reintroduces cascade non-determinism deliberately, so it is a +compatibility/fallback knob, not a routine mode. + +## 13. Code map + +| Area | File(s) | Key symbols | +|------|---------|-------------| +| Feature var | `sql/sys_vars.cc`, `sql/sql_class.h` | `rpl_use_binlog_events_for_fk_cascade` | +| SE ↔ server API | `include/mysql/service_thd_binlog.h` | `thd_binlog_cascade_delete_row()`, `thd_binlog_cascade_update_row()`, `thd_rpl_use_binlog_events_for_fk_cascade()` | +| THD state, queue, flush / discard | `sql/sql_class.{h,cc}` | `Cascade_binlog_row_event`, `pending_cascade_binlog_row_events`, `binlog_report_cascade_row()`, `flush_pending_cascade_binlog()`, `discard_pending_cascade_binlog()`, `binlog_fk_cascade_events`, `binlog_fk_cascade_derived`, `binlog_mark_fk_cascade_events()` | +| Capture / report | `storage/innobase/row/row0ins.cc` | `row_ins_foreign_check_on_constraint()`, `row_ins_find_open_table_for_cascade_binlog()`, `row_ins_allow_fk_cascade_binlog_for_table()` | +| Row-image template | `storage/innobase/handler/ha_innodb.{h,cc}` | `rebuild_template_for_cascade_binlog_row_image()` | +| Drain points | `sql/handler.{h,cc}`, `sql/log.cc`, `sql/sql_parse.cc` | calls in `ha_commit_trans`/`ha_rollback_trans`/`ha_rollback_to_savepoint`, `binlog_flush_pending_rows_event()`, drained-queue `DBUG_ASSERT` in `mysql_execute_command()` | +| Event flags / apply | `sql/log_event.h`, `sql/log_event_server.cc`, `sql/log.cc` | `FK_CASCADE_EVENTS_F`, `FK_CASCADE_DERIVED_F`, `do_apply_event()`, `prepare_pending_rows_event()`, `binlog_flush_pending_rows_event()` | +| wsrep | `sql/service_wsrep.cc`, `include/mysql/service_wsrep.h` | `wsrep_emulate_binlog()` | +| Test aid | `sql/log_event_server.cc` (`do_apply_event()`) | `DBUG_EXECUTE_IF("rpl_emulate_old_slave_fk_cascade", …)` — emulate a pre-MDEV-38243 replica (see §9.1) | + +## 14. Tests (`mysql-test/suite/rpl/`) + +- **`rpl_fk_cascade_binlog_row`** — feature OFF vs ON; asserts derived child + row events are absent (OFF) / present (ON) in the origin binlog and replica + data is correct. +- **`rpl_fk_cascade_binlog_row_ordering`** — event ordering of interleaved + cascade delete/update. +- **`rpl_fk_cascade_binlog_row_rollback`** — queued events discarded on + rollback / rollback-to-savepoint; nothing spurious is logged. +- **`rpl_fk_set_null_binlog_row`** — `SET NULL` cascade capture. +- **`rpl_fk_cascade_binlog_row_slave_option`** — origin OFF / replica ON: + replication stays correct and the replica's binlog contains only parent + events (pins the applier no-op of §10). +- **`rpl_fk_cascade_binlog_row_old_slave`** — cross-version compatibility + (§9): a strict-mode replica emulating an older MariaDB (via the + `rpl_emulate_old_slave_fk_cascade` debug keyword) applies a feature-ON + origin's `CASCADE` / `SET NULL` transactions with no error and correct data. + Debug build only. + +**Not yet covered:** cross-version replication against a *real* older `mariadbd` +binary. The apply mechanism itself is covered in-tree by `rpl_fk_cascade_binlog_row_old_slave` (§10.1). diff --git a/include/mysql/service_thd_binlog.h b/include/mysql/service_thd_binlog.h index 912f4213ee662..63ee5ccdb3b9e 100644 --- a/include/mysql/service_thd_binlog.h +++ b/include/mysql/service_thd_binlog.h @@ -20,27 +20,27 @@ extern "C" { #endif struct TABLE; -class Event_log; -class binlog_cache_data; int thd_is_current_stmt_binlog_format_row(const MYSQL_THD thd); int thd_rpl_use_binlog_events_for_fk_cascade(const MYSQL_THD thd); -void thd_binlog_mark_fk_cascade_events(MYSQL_THD thd); - -int thd_binlog_update_row(MYSQL_THD thd, struct TABLE *table, - class Event_log *bin_log, - class binlog_cache_data *cache_data, - int is_trans, unsigned long row_image, - const unsigned char *before_record, - const unsigned char *after_record); - -int thd_binlog_delete_row(MYSQL_THD thd, struct TABLE *table, - class Event_log *bin_log, - class binlog_cache_data *cache_data, - int is_trans, unsigned long row_image, - const unsigned char *before_record); +/* + Report an FK-cascade row change performed by a storage engine on a child + table. The storage engine only supplies the child TABLE and the affected + row image(s) in MySQL record format; the server decides whether and how to + binlog the change. It queues the reported rows in execution order, marks the + resulting row events (FK_CASCADE_EVENTS_F etc.), and flushes them into the + binary log at statement end / commit (discarding them on rollback). The + server copies the supplied record buffers, so the caller may reuse or free + them once the call returns. +*/ +void thd_binlog_cascade_delete_row(MYSQL_THD thd, struct TABLE *table, + const unsigned char *before_record); + +void thd_binlog_cascade_update_row(MYSQL_THD thd, struct TABLE *table, + const unsigned char *before_record, + const unsigned char *after_record); #ifdef __cplusplus } diff --git a/sql/handler.cc b/sql/handler.cc index 110c1de3109d6..f9660fca87bb1 100644 --- a/sql/handler.cc +++ b/sql/handler.cc @@ -120,14 +120,8 @@ KEY_CREATE_INFO default_key_create_info= static void flush_pending_cascade_binlog_for_thd(THD *thd) { - if (!thd) return; - - TABLE *table; - for (table = thd->open_tables; table; table = table->next) { - if (table->file) { - table->file->flush_pending_cascade_binlog(); - } - } + if (thd) + thd->flush_pending_cascade_binlog(); } static void discard_pending_cascade_binlog_for_thd(THD *thd) @@ -137,14 +131,8 @@ static void discard_pending_cascade_binlog_for_thd(THD *thd) cascade row events, it never writes to the binary log. So it must run for slave/applier threads too. Hence no thd->rgi_slave check here. */ - if (!thd) return; - - TABLE *table; - for (table = thd->open_tables; table; table = table->next) { - if (table->file) { - table->file->discard_pending_cascade_binlog(); - } - } + if (thd) + thd->discard_pending_cascade_binlog(); } /* number of entries in handlertons[] */ @@ -2462,10 +2450,10 @@ int ha_rollback_trans(THD *thd, bool all) The engines have rolled back (the whole transaction when all==true, or the current statement to its implicit savepoint when all==false). Any queued FK-cascade row events describe row changes that were just undone, so - discard and free them rather than letting them be flushed into the binary - log by a later statement or at commit. For a full rollback the queue would - also be freed when the trx is released, but discarding here keeps the - statement-rollback case correct and the behaviour consistent. + discard and free them. + The queue is owned by the THD, so it is not reclaimed by the engine + releasing its transaction; discarding here is what keeps both the + full-rollback and the statement-rollback case correct. */ if (thd->variables.rpl_use_binlog_events_for_fk_cascade || WSREP_EMULATE_BINLOG(thd)) diff --git a/sql/handler.h b/sql/handler.h index a2f85ac0303d9..e444932c963c6 100644 --- a/sql/handler.h +++ b/sql/handler.h @@ -5311,14 +5311,6 @@ class handler :public Sql_alloc bool prepare_for_row_logging(); int prepare_for_modify(bool can_set_fields, bool can_lookup); - virtual void flush_pending_cascade_binlog() {} - - /* - Discard FK-cascade row events queued for this transaction, - when transaction rolls back. - */ - virtual void discard_pending_cascade_binlog() {} - int prepare_for_insert(bool do_create); int binlog_log_row(const uchar *before_record, const uchar *after_record, diff --git a/sql/log.cc b/sql/log.cc index b90c89972e272..862db7b433823 100644 --- a/sql/log.cc +++ b/sql/log.cc @@ -8071,14 +8071,7 @@ int binlog_flush_pending_rows_event(THD *thd, bool stmt_end, if (thd->binlog_fk_cascade_events && (thd->variables.rpl_use_binlog_events_for_fk_cascade || WSREP_EMULATE_BINLOG(thd))) - { - TABLE *table; - for (table= thd->open_tables; table; table= table->next) - { - if (table->file) - table->file->flush_pending_cascade_binlog(); - } - } + thd->flush_pending_cascade_binlog(); /* Flushing cascaded row events may have created a new pending event or diff --git a/sql/sql_class.cc b/sql/sql_class.cc index d81da48861c77..8cec7bb74da16 100644 --- a/sql/sql_class.cc +++ b/sql/sql_class.cc @@ -117,6 +117,120 @@ void THD::binlog_mark_fk_cascade_events() } } +/* + Queue an FK-cascade row change reported by a storage engine, for binlogging. + The record image(s) are copied so the caller may reuse or free its buffers + once this returns. Marking the current statement as carrying cascade events + here ensures the originating statement's own pending event is flagged too. +*/ +void THD::binlog_report_cascade_row(TABLE *table, bool is_delete, + const uchar *before_record, + const uchar *after_record) +{ + DBUG_ASSERT(table); + DBUG_ASSERT(before_record); + DBUG_ASSERT(is_delete == (after_record == NULL)); + + const size_t len= table->s->reclength; + uchar *before_copy= (uchar *) my_malloc(PSI_INSTRUMENT_ME, len, MYF(MY_WME)); + uchar *after_copy= NULL; + if (!is_delete) + after_copy= (uchar *) my_malloc(PSI_INSTRUMENT_ME, len, MYF(MY_WME)); + + if (!before_copy || (!is_delete && !after_copy)) + { + my_free(before_copy); + my_free(after_copy); + return; + } + + memcpy(before_copy, before_record, len); + if (!is_delete) + memcpy(after_copy, after_record, len); + + Cascade_binlog_row_event ev; + ev.table= table; + ev.before_record= before_copy; + ev.after_record= after_copy; + ev.is_delete= is_delete; + + if (pending_cascade_binlog_row_events.append(ev)) + { + my_free(before_copy); + my_free(after_copy); + return; + } + + /* Flag the originating statement's own (already pending) row event. */ + binlog_mark_fk_cascade_events(); +} + +/* + Flush the queued FK-cascade row changes into the binary log, in the order the + cascade operations executed. Deferring here keeps the root statement's own + events ahead of the derived ones, and keeps interleaved cascade operations + in execution order. + Events written here are additionally marked FK_CASCADE_DERIVED_F. +*/ +void THD::flush_pending_cascade_binlog() +{ + if (pending_cascade_binlog_row_events.elements() == 0) + return; + + binlog_mark_fk_cascade_events(); + binlog_begin_fk_cascade_derived(); + + for (size_t i= 0; i < pending_cascade_binlog_row_events.elements(); i++) + { + Cascade_binlog_row_event &ev= pending_cascade_binlog_row_events.at(i); + TABLE *table= ev.table; + if (!table || !table->file || + table->s->tmp_table != NO_TMP_TABLE) + { + my_free(ev.before_record); + my_free(ev.after_record); + continue; + } + + MY_BITMAP *old_read_set= table->read_set; + MY_BITMAP *old_write_set= table->write_set; + MY_BITMAP *old_rpl_write_set= table->rpl_write_set; + + table->column_bitmaps_set_no_signal(&table->s->all_set, + &table->s->all_set); + if (table->rpl_write_set == NULL) + table->rpl_write_set= &table->s->all_set; + + Log_func *log_func= ev.is_delete + ? Delete_rows_log_event::binlog_row_logging_function + : Update_rows_log_event::binlog_row_logging_function; + table->file->binlog_log_row(ev.before_record, ev.after_record, log_func); + + table->column_bitmaps_set_no_signal(old_read_set, old_write_set); + table->rpl_write_set= old_rpl_write_set; + + my_free(ev.before_record); + my_free(ev.after_record); + } + + binlog_end_fk_cascade_derived(); + pending_cascade_binlog_row_events.clear(); +} + +/* + Drop the queued FK-cascade row changes without logging them. +*/ +void THD::discard_pending_cascade_binlog() +{ + for (size_t i= 0; i < pending_cascade_binlog_row_events.elements(); i++) + { + Cascade_binlog_row_event &ev= pending_cascade_binlog_row_events.at(i); + my_free(ev.before_record); + my_free(ev.after_record); + } + pending_cascade_binlog_row_events.clear(); +} + extern "C" void free_user_var(void *entry_) { user_var_entry *entry= static_cast(entry_); @@ -544,32 +658,18 @@ int thd_rpl_use_binlog_events_for_fk_cascade(const THD *thd) } extern "C" -void thd_binlog_mark_fk_cascade_events(THD *thd) -{ - thd->binlog_mark_fk_cascade_events(); -} - -extern "C" -int thd_binlog_update_row(THD *thd, TABLE *table, Event_log *bin_log, - binlog_cache_data *cache_data, int is_trans, - unsigned long row_image, - const unsigned char *before_record, - const unsigned char *after_record) +void thd_binlog_cascade_delete_row(THD *thd, TABLE *table, + const unsigned char *before_record) { - return thd->binlog_update_row(table, bin_log, cache_data, (bool) is_trans, - (enum_binlog_row_image) row_image, - before_record, after_record); + thd->binlog_report_cascade_row(table, true, before_record, NULL); } extern "C" -int thd_binlog_delete_row(THD *thd, TABLE *table, Event_log *bin_log, - binlog_cache_data *cache_data, int is_trans, - unsigned long row_image, - const unsigned char *before_record) +void thd_binlog_cascade_update_row(THD *thd, TABLE *table, + const unsigned char *before_record, + const unsigned char *after_record) { - return thd->binlog_delete_row(table, bin_log, cache_data, (bool) is_trans, - (enum_binlog_row_image) row_image, - before_record); + thd->binlog_report_cascade_row(table, false, before_record, after_record); } /* @@ -1954,6 +2054,8 @@ THD::~THD() THD *orig_thd= current_thd; THD_CHECK_SENTRY(this); DBUG_ENTER("~THD()"); + /* Backstop: free any FK-cascade row buffers not drained by commit/rollback */ + discard_pending_cascade_binlog(); /* Make sure threads are not available via server_threads. */ assert_not_linked(); if (m_psi) diff --git a/sql/sql_class.h b/sql/sql_class.h index a7789b0a7d9c3..c74c6dce119ef 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -3867,6 +3867,30 @@ class THD: public THD_count, /* this must be first */ void binlog_begin_fk_cascade_derived() { binlog_fk_cascade_derived= true; } void binlog_end_fk_cascade_derived() { binlog_fk_cascade_derived= false; } + /* + Queue of FK-cascade row changes reported by a storage engine during the + current statement, awaiting binlogging. The server owns this queue and its + lifecycle: the engine only reports rows via binlog_report_cascade_row(); + flush_pending_cascade_binlog() writes them to the binlog cache at statement + end / commit, and discard_pending_cascade_binlog() drops them on rollback. + Buffers are server-allocated copies of the engine-supplied record images. + */ + struct Cascade_binlog_row_event + { + TABLE *table; + uchar *before_record; + uchar *after_record; /* NULL for a cascade delete */ + bool is_delete; + }; + Dynamic_array + pending_cascade_binlog_row_events{PSI_INSTRUMENT_MEM}; + + void binlog_report_cascade_row(TABLE *table, bool is_delete, + const uchar *before_record, + const uchar *after_record); + void flush_pending_cascade_binlog(); + void discard_pending_cascade_binlog(); + void issue_unsafe_warnings(); void reset_unsafe_warnings() { binlog_unsafe_warning_flags= 0; } diff --git a/sql/sql_parse.cc b/sql/sql_parse.cc index 84ef1c783b3cf..eacbf9f7c886c 100644 --- a/sql/sql_parse.cc +++ b/sql/sql_parse.cc @@ -6005,6 +6005,23 @@ mysql_execute_command(THD *thd, bool is_called_from_prepared_stmt) /* Free tables. Set stage 'closing tables' */ close_thread_tables_for_query(thd); + /* + The FK-cascade row queue holds raw TABLE pointers into the tables that + were just closed. It must already have been drained, either by the + statement-end row-event flush (binlog_flush_pending_rows_event() -> + THD::flush_pending_cascade_binlog()) or by the commit/rollback above. + A leftover entry would be flushed by some later statement against a freed + TABLE, and the "!table || !table->file" guard in + THD::flush_pending_cascade_binlog() cannot detect a TABLE that has been + freed and its memory reused. + + Restricted to the top level: the commit/rollback that drains the queue is + itself under "! thd->in_sub_stmt", so a substatement legitimately leaves + its rows queued for the enclosing statement to flush. + */ + DBUG_ASSERT(thd->in_sub_stmt || + thd->pending_cascade_binlog_row_events.elements() == 0); + #ifndef DBUG_OFF if (lex->sql_command != SQLCOM_SET_OPTION && ! thd->in_sub_stmt) DEBUG_SYNC(thd, "execute_command_after_close_tables"); diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc index eb98fe740bd4f..3244772dba657 100644 --- a/storage/innobase/handler/ha_innodb.cc +++ b/storage/innobase/handler/ha_innodb.cc @@ -8872,98 +8872,6 @@ ha_innobase::update_row( DBUG_RETURN(err); } -void -ha_innobase::flush_pending_cascade_binlog() -{ - if (!m_user_thd) { - return; - } - trx_t* trx = thd_to_trx(m_user_thd); - const bool emulate_binlog= -#ifdef WITH_WSREP - wsrep_emulate_binlog(m_user_thd); -#else - false; -#endif - - if (!thd_rpl_use_binlog_events_for_fk_cascade(m_user_thd) && - !emulate_binlog) { - return; - } - - if (trx == NULL || trx->pending_cascade_binlog_row_events.empty()) { - return; - } - - m_user_thd->binlog_mark_fk_cascade_events(); - - /* - Everything logged from here until the end of the loop is a - cascade-derived row event; mark it so that it is distinguishable from - the originating statement's own row events (which were already logged - above with only FK_CASCADE_EVENTS_F). - */ - m_user_thd->binlog_begin_fk_cascade_derived(); - - for (auto& ev : trx->pending_cascade_binlog_row_events) { - if (ev.table == NULL || ev.table->file == NULL) { - if (ev.before_record) { - my_free(ev.before_record); - } - if (ev.after_record) { - my_free(ev.after_record); - } - continue; - } - - if (ev.table->s->tmp_table != NO_TMP_TABLE) { - if (ev.before_record) { - my_free(ev.before_record); - } - if (ev.after_record) { - my_free(ev.after_record); - } - continue; - } - - MY_BITMAP* old_read_set = ev.table->read_set; - MY_BITMAP* old_write_set = ev.table->write_set; - MY_BITMAP* old_rpl_write_set = ev.table->rpl_write_set; - - ev.table->column_bitmaps_set_no_signal( - &ev.table->s->all_set, &ev.table->s->all_set); - if (ev.table->rpl_write_set == NULL) { - ev.table->rpl_write_set = &ev.table->s->all_set; - } - - Log_func* log_func = reinterpret_cast(ev.log_func); - ev.table->file->binlog_log_row(ev.before_record, - ev.after_record, - log_func); - - ev.table->column_bitmaps_set_no_signal(old_read_set, old_write_set); - ev.table->rpl_write_set = old_rpl_write_set; - - my_free(ev.before_record); - my_free(ev.after_record); - } - - m_user_thd->binlog_end_fk_cascade_derived(); - - trx->pending_cascade_binlog_row_events.clear(); -} - -void -ha_innobase::discard_pending_cascade_binlog() -{ - if (!m_user_thd) { - return; - } - if (trx_t* trx = thd_to_trx(m_user_thd)) { - trx->free_cascade_binlog_row_events(); - } -} - /**********************************************************************//** Deletes a row given as the parameter. @return error number or 0 */ diff --git a/storage/innobase/handler/ha_innodb.h b/storage/innobase/handler/ha_innodb.h index 8b2c165eb0e0b..6670a557bd1e0 100644 --- a/storage/innobase/handler/ha_innodb.h +++ b/storage/innobase/handler/ha_innodb.h @@ -121,10 +121,6 @@ class ha_innobase final : public handler int update_row(const uchar * old_data, const uchar * new_data) override; - void flush_pending_cascade_binlog() override; - - void discard_pending_cascade_binlog() override; - int delete_row(const uchar * buf) override; bool was_semi_consistent_read() override; diff --git a/storage/innobase/include/trx0trx.h b/storage/innobase/include/trx0trx.h index 5dc88011f9cbe..b8c1de3e471a4 100644 --- a/storage/innobase/include/trx0trx.h +++ b/storage/innobase/include/trx0trx.h @@ -46,13 +46,6 @@ struct rw_trx_hash_element_t; class ha_handler_stats; struct TABLE; -struct trx_cascade_binlog_row_event { - TABLE* table; - unsigned char* before_record; - unsigned char* after_record; - void* log_func; -}; - /******************************************************************//** Set detailed error message for the transaction. */ void @@ -971,12 +964,6 @@ struct trx_t : ilist_node<> trx_mod_tables_t mod_tables; /*!< List of tables that were modified by this transaction */ - std::vector pending_cascade_binlog_row_events; - - /** Free the events of any queued FK-cascade binlog row events and empty - the list. Used both to discard events whose row changes are being rolled - back and to reclaim memory for events that were never flushed. */ - void free_cascade_binlog_row_events(); /*------------------------------*/ char* detailed_error; /*!< detailed error message for last error, or empty. */ diff --git a/storage/innobase/row/row0ins.cc b/storage/innobase/row/row0ins.cc index 7cfe57f2feaf9..9d824304ca380 100644 --- a/storage/innobase/row/row0ins.cc +++ b/storage/innobase/row/row0ins.cc @@ -62,33 +62,6 @@ Created 4/20/1996 Heikki Tuuri TABLE *find_fk_open_table(THD *thd, const char *db, size_t db_len, const char *table, size_t table_len); -static bool row_ins_fk_cascade_delete_binlog_row(THD *thd, TABLE *table, - Event_log *bin_log, - binlog_cache_data *cache_data, - bool is_transactional, - ulong row_image, - const uchar *before_record, - const uchar *after_record - __attribute__((unused))) -{ - return thd_binlog_delete_row(thd, table, bin_log, cache_data, - (int) is_transactional, row_image, - before_record); -} - -static bool row_ins_fk_cascade_update_binlog_row(THD *thd, TABLE *table, - Event_log *bin_log, - binlog_cache_data *cache_data, - bool is_transactional, - ulong row_image, - const uchar *before_record, - const uchar *after_record) -{ - return thd_binlog_update_row(thd, table, bin_log, cache_data, - (int) is_transactional, row_image, - before_record, after_record); -} - static TABLE* row_ins_find_open_table_for_cascade_binlog( trx_t* trx, @@ -1599,48 +1572,24 @@ row_ins_foreign_check_on_constraint( if (can_cascade_binlog && (cascade->is_delete == PLAIN_DELETE || have_after_image)) { /* - Queue the cascade row event (both the DELETE and the UPDATE case) - and flush it later from ha_innobase::flush_pending_cascade_binlog(). - Both kinds go through the same queue so that events are written to - the binary log in the same order the cascade operations were - executed. Logging deletes immediately here would place every cascade - delete ahead of every deferred update within a statement, which - reorders events that touch the same row and can make the replica - apply an update to an already-deleted row. + Report the cascade row change to the server (both the DELETE and + the UPDATE case). The server queues the reported rows and writes + them to the binary log in the same order the cascade operations + were executed, after the originating statement's own events. + Logging deletes immediately here would place every cascade delete + ahead of every deferred update within a statement, which reorders + events that touch the same row and can make the replica apply an + update to an already-deleted row. The server copies the record + images, so the temp-heap buffers can be reused/freed afterwards. */ - const bool is_delete = (cascade->is_delete == PLAIN_DELETE); - const ulint len = child_mysql_table->s->reclength; - unsigned char* before_copy = static_cast( - my_malloc(PSI_INSTRUMENT_ME, len, MYF(MY_WME))); - unsigned char* after_copy = NULL; - if (!is_delete) { - after_copy = static_cast( - my_malloc(PSI_INSTRUMENT_ME, len, MYF(MY_WME))); - } - - if (before_copy != NULL && (is_delete || after_copy != NULL)) { - thd_binlog_mark_fk_cascade_events(trx->mysql_thd); - memcpy(before_copy, before_mysql_rec, len); - if (!is_delete) { - memcpy(after_copy, after_mysql_rec, len); - } - - trx_cascade_binlog_row_event ev; - ev.table = child_mysql_table; - ev.before_record = before_copy; - ev.after_record = after_copy; - ev.log_func = reinterpret_cast( - is_delete - ? row_ins_fk_cascade_delete_binlog_row - : row_ins_fk_cascade_update_binlog_row); - trx->pending_cascade_binlog_row_events.push_back(ev); + if (cascade->is_delete == PLAIN_DELETE) { + thd_binlog_cascade_delete_row( + trx->mysql_thd, child_mysql_table, + before_mysql_rec); } else { - if (before_copy != NULL) { - my_free(before_copy); - } - if (after_copy != NULL) { - my_free(after_copy); - } + thd_binlog_cascade_update_row( + trx->mysql_thd, child_mysql_table, + before_mysql_rec, after_mysql_rec); } } diff --git a/storage/innobase/trx/trx0trx.cc b/storage/innobase/trx/trx0trx.cc index 511e2aa1dc594..5632981f1a2f5 100644 --- a/storage/innobase/trx/trx0trx.cc +++ b/storage/innobase/trx/trx0trx.cc @@ -161,21 +161,6 @@ trx_init( #endif /* WITH_WSREP */ } -void trx_t::free_cascade_binlog_row_events() -{ - /* The before/after record images are allocated in - row_ins_foreign_check_on_constraint() and are normally freed when - ha_innobase::flush_pending_cascade_binlog() emits them at commit. - If the transaction rolls back, or the events are never flushed, - the buffers would otherwise leak, so free them here. - Note: my_free(NULL) is a no-op. */ - for (auto& ev : pending_cascade_binlog_row_events) { - my_free(ev.before_record); - my_free(ev.after_record); - } - pending_cascade_binlog_row_events.clear(); -} - /** For managing the life-cycle of the trx_t instance that we get from the pool. */ struct TrxFactory { @@ -194,9 +179,6 @@ struct TrxFactory { new(&trx->mod_tables) trx_mod_tables_t(); - new(&trx->pending_cascade_binlog_row_events) - std::vector(); - new(&trx->lock.table_locks) lock_list(); new(&trx->read_view) ReadView(); @@ -262,9 +244,6 @@ struct TrxFactory { trx->mod_tables.~trx_mod_tables_t(); - trx->free_cascade_binlog_row_events(); - trx->pending_cascade_binlog_row_events.~vector(); - ut_ad(!trx->read_view.is_open()); trx->lock.table_locks.~lock_list(); @@ -432,7 +411,6 @@ void trx_t::free() noexcept check_unique_secondary= true; check_foreigns= true; - free_cascade_binlog_row_events(); assert_freed(); trx_sys.rw_trx_hash.put_pins(this); mysql_thd= nullptr;