From 8225c64f7ed66e9a45b9ab271b74e7bf21bc10f8 Mon Sep 17 00:00:00 2001 From: Santosh Praneeth Banda Date: Thu, 13 Nov 2014 11:57:06 -0800 Subject: [PATCH] Option to run triggers on slave for row-based events Summary: Port the slave_run_triggers_for_rbr feature from mariadb 10.1.1. When using statement based replication slave executes the sql statments which runs the slave side triggers. Since in row based replication, slave applies the row events directly to the storage engine, triggers on the slave table are not executed. Add functionality to run triggers on slave side when executing row based events. The following triggers are invoked: * Update_row_event runs an UPDATE trigger * Delete_row_event runs a DELETE trigger * Write_row_event action depends on whether the operation will require foreign key checks: 1) when FK checks are not necessary, the operation will invoke a DELETE trigger if the record to be modified existed in the table. After that, an INSERT trigger will be invoked. 2) when FK checks are necessary, either an UPDATE or or a combination of DELETE and INSERT triggers will be invoked. slave_run_triggers_for_rbr option controls the feature. Default value is NO which don't invoke trigger for row-based events; Setting the option to YES will cause the SQL slave thread to invoke triggers for row based events; setting it to LOGGING will also cause the changes made by the triggers to be written into the binary log. There is a basic protection against triggers being invoked both on the master and slave. If the master modifies a table that has triggers, it will produce row-based binlog events with the "triggers were invoked for this event" flag. The slave will not invoke any triggers for flagged events. Test Plan: mtr tests Reviewers: jtolmer Reviewed By: jtolmer --- mysql-test/include/have_rbr_triggers.inc | 4 + .../r/mysqld--help-notwin-profiling.result | 10 + mysql-test/r/mysqld--help-notwin.result | 10 + .../suite/rpl/r/rpl_row_triggers.result | 258 ++++++++++++++++++ .../suite/rpl/r/rpl_row_triggers_sbr.result | 17 ++ mysql-test/suite/rpl/t/rpl_row_triggers.test | 256 +++++++++++++++++ .../suite/rpl/t/rpl_row_triggers_sbr.test | 37 +++ .../r/slave_run_triggers_for_rbr_basic.result | 27 ++ .../t/slave_run_triggers_for_rbr_basic.test | 23 ++ sql/log_event.cc | 248 ++++++++++++++--- sql/log_event.h | 16 +- sql/mysqld.cc | 1 + sql/mysqld.h | 1 + sql/rpl_utility.h | 1 + sql/sql_class.h | 5 + sql/sql_delete.cc | 22 +- sql/sql_insert.cc | 51 +--- sql/sql_insert.h | 1 - sql/sql_load.cc | 4 +- sql/sql_update.cc | 22 +- sql/sys_vars.cc | 15 + sql/table.cc | 75 ++++- sql/table.h | 7 + 23 files changed, 986 insertions(+), 125 deletions(-) create mode 100644 mysql-test/include/have_rbr_triggers.inc create mode 100644 mysql-test/suite/rpl/r/rpl_row_triggers.result create mode 100644 mysql-test/suite/rpl/r/rpl_row_triggers_sbr.result create mode 100644 mysql-test/suite/rpl/t/rpl_row_triggers.test create mode 100644 mysql-test/suite/rpl/t/rpl_row_triggers_sbr.test create mode 100644 mysql-test/suite/sys_vars/r/slave_run_triggers_for_rbr_basic.result create mode 100644 mysql-test/suite/sys_vars/t/slave_run_triggers_for_rbr_basic.test diff --git a/mysql-test/include/have_rbr_triggers.inc b/mysql-test/include/have_rbr_triggers.inc new file mode 100644 index 000000000000..61f328d00d3b --- /dev/null +++ b/mysql-test/include/have_rbr_triggers.inc @@ -0,0 +1,4 @@ +if (`select count(*) = 0 from information_schema.session_variables where variable_name = 'slave_run_triggers_for_rbr'`) +{ + skip RBR triggers are not available; +} diff --git a/mysql-test/r/mysqld--help-notwin-profiling.result b/mysql-test/r/mysqld--help-notwin-profiling.result index 5b6026b92699..ac0af730aff9 100644 --- a/mysql-test/r/mysqld--help-notwin-profiling.result +++ b/mysql-test/r/mysqld--help-notwin-profiling.result @@ -1025,6 +1025,15 @@ The following options may be given as the first argument: the slave will always pick the most suitable algorithm for any given scenario. (Default: INDEX_SCAN, TABLE_SCAN). + --slave-run-triggers-for-rbr=name + Modes for how triggers in row-base replication on slave + side will be executed. Legal values are NO (default), YES + and LOGGING. NO means that trigger for RBR will not be + running on slave. YES and LOGGING means that triggers + will be running on slave, if there was not triggers + running on the master for the statement. LOGGING also + means results of that the executed triggers work will be + written to the binlog. --slave-skip-errors=name Tells the slave thread to continue replication when a query event returns an error from the provided list @@ -1462,6 +1471,7 @@ slave-net-timeout 3600 slave-parallel-workers 0 slave-pending-jobs-size-max 16777216 slave-rows-search-algorithms TABLE_SCAN,INDEX_SCAN +slave-run-triggers-for-rbr NO slave-skip-errors (No default value) slave-sql-verify-checksum TRUE slave-transaction-retries 10 diff --git a/mysql-test/r/mysqld--help-notwin.result b/mysql-test/r/mysqld--help-notwin.result index c05f446d252c..898718a7c636 100644 --- a/mysql-test/r/mysqld--help-notwin.result +++ b/mysql-test/r/mysqld--help-notwin.result @@ -1023,6 +1023,15 @@ The following options may be given as the first argument: the slave will always pick the most suitable algorithm for any given scenario. (Default: INDEX_SCAN, TABLE_SCAN). + --slave-run-triggers-for-rbr=name + Modes for how triggers in row-base replication on slave + side will be executed. Legal values are NO (default), YES + and LOGGING. NO means that trigger for RBR will not be + running on slave. YES and LOGGING means that triggers + will be running on slave, if there was not triggers + running on the master for the statement. LOGGING also + means results of that the executed triggers work will be + written to the binlog. --slave-skip-errors=name Tells the slave thread to continue replication when a query event returns an error from the provided list @@ -1459,6 +1468,7 @@ slave-net-timeout 3600 slave-parallel-workers 0 slave-pending-jobs-size-max 16777216 slave-rows-search-algorithms TABLE_SCAN,INDEX_SCAN +slave-run-triggers-for-rbr NO slave-skip-errors (No default value) slave-sql-verify-checksum TRUE slave-transaction-retries 10 diff --git a/mysql-test/suite/rpl/r/rpl_row_triggers.result b/mysql-test/suite/rpl/r/rpl_row_triggers.result new file mode 100644 index 000000000000..3ca1c57109ca --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_row_triggers.result @@ -0,0 +1,258 @@ +include/master-slave.inc +Warnings: +Note #### Sending passwords in plain text without SSL/TLS is extremely insecure. +Note #### Storing MySQL user name or password information in the master info repository is not secure and is therefore not recommended. Please consider using the USER and PASSWORD connection options for START SLAVE; see the 'START SLAVE Syntax' in the MySQL Manual for more information. +[connection master] +# Test of row replication with triggers on the slave side +CREATE TABLE t1 (C1 CHAR(1) primary key, C2 CHAR(1)); +SELECT * FROM t1; +C1 C2 +SET @old_slave_exec_mode= @@global.slave_exec_mode; +SET @old_slave_run_triggers_for_rbr= @@global.slave_run_triggers_for_rbr; +SET @@global.slave_exec_mode= IDEMPOTENT; +SET @@global.slave_run_triggers_for_rbr= YES; +SELECT * FROM t1; +C1 C2 +create table t2 (id char(2) primary key, cnt int, o char(1), n char(1)); +insert into t2 values +('u0', 0, ' ', ' '),('u1', 0, ' ', ' '), +('d0', 0, ' ', ' '),('d1', 0, ' ', ' '), +('i0', 0, ' ', ' '),('i1', 0, ' ', ' '); +create trigger t1_cnt_b before update on t1 for each row +update t2 set cnt=cnt+1, o=old.C1, n=new.C1 where id = 'u0'; +create trigger t1_cnt_db before delete on t1 for each row +update t2 set cnt=cnt+1, o=old.C1, n=' ' where id = 'd0'; +create trigger t1_cnt_ib before insert on t1 for each row +update t2 set cnt=cnt+1, n=new.C1, o=' ' where id = 'i0'; +create trigger t1_cnt_a after update on t1 for each row +update t2 set cnt=cnt+1, o=old.C1, n=new.C1 where id = 'u1'; +create trigger t1_cnt_da after delete on t1 for each row +update t2 set cnt=cnt+1, o=old.C1, n=' ' where id = 'd1'; +create trigger t1_cnt_ia after insert on t1 for each row +update t2 set cnt=cnt+1, n=new.C1, o=' ' where id = 'i1'; +SELECT * FROM t2 order by id; +id cnt o n +d0 0 +d1 0 +i0 0 +i1 0 +u0 0 +u1 0 +# INSERT triggers test +insert into t1 values ('a','b'); +SELECT * FROM t2 order by id; +id cnt o n +d0 0 +d1 0 +i0 1 a +i1 1 a +u0 0 +u1 0 +# UPDATE triggers test +update t1 set C1= 'd'; +SELECT * FROM t2 order by id; +id cnt o n +d0 0 +d1 0 +i0 1 a +i1 1 a +u0 1 a d +u1 1 a d +# DELETE triggers test +delete from t1 where C1='d'; +SELECT * FROM t2 order by id; +id cnt o n +d0 1 d +d1 1 d +i0 1 a +i1 1 a +u0 1 a d +u1 1 a d +# INSERT triggers which cause also UPDATE test (insert duplicate row) +insert into t1 values ('0','1'); +SELECT * FROM t2 order by id; +id cnt o n +d0 1 d +d1 1 d +i0 2 0 +i1 2 0 +u0 1 a d +u1 1 a d +insert into t1 values ('0','1'); +SELECT * FROM t2 order by id; +id cnt o n +d0 1 d +d1 1 d +i0 3 0 +i1 3 0 +u0 2 0 0 +u1 2 0 0 +# INSERT triggers which cause also DELETE test +# (insert duplicate row in table referenced by foreign key) +insert into t1 values ('1','1'); +CREATE TABLE t3 (C1 CHAR(1) primary key, FOREIGN KEY (C1) REFERENCES t1(C1) ); +insert into t1 values ('1','1'); +SELECT * FROM t2 order by id; +id cnt o n +d0 2 1 +d1 2 1 +i0 5 1 +i1 5 1 +u0 2 0 0 +u1 2 0 0 +drop table t3,t1; +SET @@global.slave_exec_mode= @old_slave_exec_mode; +SET @@global.slave_run_triggers_for_rbr= @old_slave_run_triggers_for_rbr; +drop table t2; +CREATE TABLE t1 (i INT) ENGINE=InnoDB; +CREATE TABLE t2 (i INT) ENGINE=InnoDB; +SET @old_slave_run_triggers_for_rbr= @@global.slave_run_triggers_for_rbr; +SET GLOBAL slave_run_triggers_for_rbr=YES; +CREATE TRIGGER tr AFTER INSERT ON t1 FOR EACH ROW +INSERT INTO t2 VALUES (new.i); +BEGIN; +INSERT INTO t1 VALUES (1); +INSERT INTO t1 VALUES (2); +COMMIT; +select * from t2; +i +1 +2 +SET @@global.slave_run_triggers_for_rbr= @old_slave_run_triggers_for_rbr; +drop tables t2,t1; +# Triggers on slave do not work if master has some +CREATE TABLE t1 (C1 CHAR(1) primary key, C2 CHAR(1)); +SELECT * FROM t1; +C1 C2 +create trigger t1_dummy before delete on t1 for each row +set @dummy= 1; +SET @old_slave_exec_mode= @@global.slave_exec_mode; +SET @old_slave_run_triggers_for_rbr= @@global.slave_run_triggers_for_rbr; +SET @@global.slave_exec_mode= IDEMPOTENT; +SET @@global.slave_run_triggers_for_rbr= YES; +SELECT * FROM t1; +C1 C2 +create table t2 (id char(2) primary key, cnt int, o char(1), n char(1)); +insert into t2 values +('u0', 0, ' ', ' '),('u1', 0, ' ', ' '), +('d0', 0, ' ', ' '),('d1', 0, ' ', ' '), +('i0', 0, ' ', ' '),('i1', 0, ' ', ' '); +create trigger t1_cnt_b before update on t1 for each row +update t2 set cnt=cnt+1, o=old.C1, n=new.C1 where id = 'u0'; +create trigger t1_cnt_ib before insert on t1 for each row +update t2 set cnt=cnt+1, n=new.C1, o=' ' where id = 'i0'; +create trigger t1_cnt_a after update on t1 for each row +update t2 set cnt=cnt+1, o=old.C1, n=new.C1 where id = 'u1'; +create trigger t1_cnt_da after delete on t1 for each row +update t2 set cnt=cnt+1, o=old.C1, n=' ' where id = 'd1'; +create trigger t1_cnt_ia after insert on t1 for each row +update t2 set cnt=cnt+1, n=new.C1, o=' ' where id = 'i1'; +SELECT * FROM t2 order by id; +id cnt o n +d0 0 +d1 0 +i0 0 +i1 0 +u0 0 +u1 0 +# INSERT triggers test +insert into t1 values ('a','b'); +SELECT * FROM t2 order by id; +id cnt o n +d0 0 +d1 0 +i0 0 +i1 0 +u0 0 +u1 0 +# UPDATE triggers test +update t1 set C1= 'd'; +SELECT * FROM t2 order by id; +id cnt o n +d0 0 +d1 0 +i0 0 +i1 0 +u0 0 +u1 0 +# DELETE triggers test +delete from t1 where C1='d'; +SELECT * FROM t2 order by id; +id cnt o n +d0 0 +d1 0 +i0 0 +i1 0 +u0 0 +u1 0 +# INSERT triggers which cause also UPDATE test (insert duplicate row) +insert into t1 values ('0','1'); +SELECT * FROM t2 order by id; +id cnt o n +d0 0 +d1 0 +i0 1 0 +i1 1 0 +u0 0 +u1 0 +insert into t1 values ('0','1'); +SELECT * FROM t2 order by id; +id cnt o n +d0 0 +d1 0 +i0 1 0 +i1 1 0 +u0 0 +u1 0 +# INSERT triggers which cause also DELETE test +# (insert duplicate row in table referenced by foreign key) +insert into t1 values ('1','1'); +CREATE TABLE t3 (C1 CHAR(1) primary key, FOREIGN KEY (C1) REFERENCES t1(C1) ); +insert into t1 values ('1','1'); +SELECT * FROM t2 order by id; +id cnt o n +d0 0 +d1 0 +i0 2 1 +i1 2 1 +u0 0 +u1 0 +drop table t3,t1; +SET @@global.slave_exec_mode= @old_slave_exec_mode; +SET @@global.slave_run_triggers_for_rbr= @old_slave_run_triggers_for_rbr; +drop table t2; +# +# MDEV-5513: Trigger is applied to the rows after first one +# +create table t1 (a int, b int); +create table tlog (a int); +set sql_log_bin=0; +create trigger tr1 after insert on t1 for each row insert into tlog values (1); +set sql_log_bin=1; +set @slave_run_triggers_for_rbr.saved = @@slave_run_triggers_for_rbr; +set global slave_run_triggers_for_rbr=1; +create trigger tr2 before insert on t1 for each row set new.b = new.a; +insert into t1 values (1,10),(2,20),(3,30); +select * from t1; +a b +1 10 +2 20 +3 30 +# +# Verify slave skips running triggers if master ran and logged the row events for triggers +# +create table t4(a int, b int); +delete from tlog; +create trigger tr4 before insert on t4 for each row insert into tlog values (1); +insert into t4 values (1, 10),(2, 20); +select * from t4; +a b +1 10 +2 20 +select * from tlog; +a +1 +1 +set global slave_run_triggers_for_rbr = @slave_run_triggers_for_rbr.saved; +drop table t1, tlog, t4; +include/rpl_end.inc diff --git a/mysql-test/suite/rpl/r/rpl_row_triggers_sbr.result b/mysql-test/suite/rpl/r/rpl_row_triggers_sbr.result new file mode 100644 index 000000000000..6297ea1c53fc --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_row_triggers_sbr.result @@ -0,0 +1,17 @@ +include/master-slave.inc +Warnings: +Note #### Sending passwords in plain text without SSL/TLS is extremely insecure. +Note #### Storing MySQL user name or password information in the master info repository is not secure and is therefore not recommended. Please consider using the USER and PASSWORD connection options for START SLAVE; see the 'START SLAVE Syntax' in the MySQL Manual for more information. +[connection master] +set binlog_format = row; +create table t1 (i int); +create table t2 (i int); +SET @old_slave_run_triggers_for_rbr= @@global.slave_run_triggers_for_rbr; +set global slave_run_triggers_for_rbr=YES; +create trigger tr_before before insert on t1 for each row +insert into t2 values (1); +insert into t1 values (1); +include/wait_for_slave_sql_error_and_skip.inc [errno=1666] +drop tables t1,t2; +SET @@global.slave_run_triggers_for_rbr= @old_slave_run_triggers_for_rbr; +include/rpl_end.inc diff --git a/mysql-test/suite/rpl/t/rpl_row_triggers.test b/mysql-test/suite/rpl/t/rpl_row_triggers.test new file mode 100644 index 000000000000..ccf08ef43ef6 --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_row_triggers.test @@ -0,0 +1,256 @@ +-- source include/have_binlog_format_row.inc +-- source include/have_rbr_triggers.inc +-- source include/have_innodb.inc +-- source include/master-slave.inc + +-- echo # Test of row replication with triggers on the slave side +connection master; +CREATE TABLE t1 (C1 CHAR(1) primary key, C2 CHAR(1)); +SELECT * FROM t1; + +sync_slave_with_master; + +connection slave; +SET @old_slave_exec_mode= @@global.slave_exec_mode; +SET @old_slave_run_triggers_for_rbr= @@global.slave_run_triggers_for_rbr; +SET @@global.slave_exec_mode= IDEMPOTENT; +SET @@global.slave_run_triggers_for_rbr= YES; +SELECT * FROM t1; +create table t2 (id char(2) primary key, cnt int, o char(1), n char(1)); +insert into t2 values + ('u0', 0, ' ', ' '),('u1', 0, ' ', ' '), + ('d0', 0, ' ', ' '),('d1', 0, ' ', ' '), + ('i0', 0, ' ', ' '),('i1', 0, ' ', ' '); +create trigger t1_cnt_b before update on t1 for each row + update t2 set cnt=cnt+1, o=old.C1, n=new.C1 where id = 'u0'; +create trigger t1_cnt_db before delete on t1 for each row + update t2 set cnt=cnt+1, o=old.C1, n=' ' where id = 'd0'; +create trigger t1_cnt_ib before insert on t1 for each row + update t2 set cnt=cnt+1, n=new.C1, o=' ' where id = 'i0'; +create trigger t1_cnt_a after update on t1 for each row + update t2 set cnt=cnt+1, o=old.C1, n=new.C1 where id = 'u1'; +create trigger t1_cnt_da after delete on t1 for each row + update t2 set cnt=cnt+1, o=old.C1, n=' ' where id = 'd1'; +create trigger t1_cnt_ia after insert on t1 for each row + update t2 set cnt=cnt+1, n=new.C1, o=' ' where id = 'i1'; +SELECT * FROM t2 order by id; + +connection master; +--echo # INSERT triggers test +insert into t1 values ('a','b'); + +sync_slave_with_master; + +connection slave; +SELECT * FROM t2 order by id; + +connection master; +--echo # UPDATE triggers test +update t1 set C1= 'd'; +sync_slave_with_master; + +connection slave; +SELECT * FROM t2 order by id; + +connection master; +--echo # DELETE triggers test +delete from t1 where C1='d'; + +sync_slave_with_master; + +connection slave; +SELECT * FROM t2 order by id; +--echo # INSERT triggers which cause also UPDATE test (insert duplicate row) +insert into t1 values ('0','1'); +SELECT * FROM t2 order by id; + +connection master; +insert into t1 values ('0','1'); + +sync_slave_with_master; + +connection slave; +SELECT * FROM t2 order by id; +--echo # INSERT triggers which cause also DELETE test +--echo # (insert duplicate row in table referenced by foreign key) +insert into t1 values ('1','1'); + +connection master; +CREATE TABLE t3 (C1 CHAR(1) primary key, FOREIGN KEY (C1) REFERENCES t1(C1) ); +insert into t1 values ('1','1'); + +sync_slave_with_master; + +connection slave; +SELECT * FROM t2 order by id; + +connection master; +drop table t3,t1; + +sync_slave_with_master; + +connection slave; +SET @@global.slave_exec_mode= @old_slave_exec_mode; +SET @@global.slave_run_triggers_for_rbr= @old_slave_run_triggers_for_rbr; +drop table t2; + +--connection master +CREATE TABLE t1 (i INT) ENGINE=InnoDB; +CREATE TABLE t2 (i INT) ENGINE=InnoDB; + +--sync_slave_with_master +SET @old_slave_run_triggers_for_rbr= @@global.slave_run_triggers_for_rbr; +SET GLOBAL slave_run_triggers_for_rbr=YES; +CREATE TRIGGER tr AFTER INSERT ON t1 FOR EACH ROW + INSERT INTO t2 VALUES (new.i); + +--connection master +BEGIN; +INSERT INTO t1 VALUES (1); +INSERT INTO t1 VALUES (2); +COMMIT; +--sync_slave_with_master +select * from t2; +SET @@global.slave_run_triggers_for_rbr= @old_slave_run_triggers_for_rbr; +--connection master +drop tables t2,t1; + +--sync_slave_with_master + +-- echo # Triggers on slave do not work if master has some + +connection master; +CREATE TABLE t1 (C1 CHAR(1) primary key, C2 CHAR(1)); +SELECT * FROM t1; +create trigger t1_dummy before delete on t1 for each row + set @dummy= 1; + +sync_slave_with_master; + +connection slave; +SET @old_slave_exec_mode= @@global.slave_exec_mode; +SET @old_slave_run_triggers_for_rbr= @@global.slave_run_triggers_for_rbr; +SET @@global.slave_exec_mode= IDEMPOTENT; +SET @@global.slave_run_triggers_for_rbr= YES; +SELECT * FROM t1; +create table t2 (id char(2) primary key, cnt int, o char(1), n char(1)); +insert into t2 values + ('u0', 0, ' ', ' '),('u1', 0, ' ', ' '), + ('d0', 0, ' ', ' '),('d1', 0, ' ', ' '), + ('i0', 0, ' ', ' '),('i1', 0, ' ', ' '); +create trigger t1_cnt_b before update on t1 for each row + update t2 set cnt=cnt+1, o=old.C1, n=new.C1 where id = 'u0'; +create trigger t1_cnt_ib before insert on t1 for each row + update t2 set cnt=cnt+1, n=new.C1, o=' ' where id = 'i0'; +create trigger t1_cnt_a after update on t1 for each row + update t2 set cnt=cnt+1, o=old.C1, n=new.C1 where id = 'u1'; +create trigger t1_cnt_da after delete on t1 for each row + update t2 set cnt=cnt+1, o=old.C1, n=' ' where id = 'd1'; +create trigger t1_cnt_ia after insert on t1 for each row + update t2 set cnt=cnt+1, n=new.C1, o=' ' where id = 'i1'; +SELECT * FROM t2 order by id; + +connection master; +--echo # INSERT triggers test +insert into t1 values ('a','b'); + +sync_slave_with_master; + +connection slave; +SELECT * FROM t2 order by id; +connection master; +--echo # UPDATE triggers test +update t1 set C1= 'd'; + +sync_slave_with_master; + +connection slave; +SELECT * FROM t2 order by id; + +connection master; +--echo # DELETE triggers test +delete from t1 where C1='d'; + +sync_slave_with_master; + +connection slave; +SELECT * FROM t2 order by id; +--echo # INSERT triggers which cause also UPDATE test (insert duplicate row) +insert into t1 values ('0','1'); +SELECT * FROM t2 order by id; + + +connection master; +insert into t1 values ('0','1'); + +sync_slave_with_master; + +connection slave; +SELECT * FROM t2 order by id; +--echo # INSERT triggers which cause also DELETE test +--echo # (insert duplicate row in table referenced by foreign key) +insert into t1 values ('1','1'); + +connection master; +CREATE TABLE t3 (C1 CHAR(1) primary key, FOREIGN KEY (C1) REFERENCES t1(C1) ); +insert into t1 values ('1','1'); + +sync_slave_with_master; + +connection slave; +SELECT * FROM t2 order by id; + +connection master; +drop table t3,t1; + +sync_slave_with_master; + +connection slave; +SET @@global.slave_exec_mode= @old_slave_exec_mode; +SET @@global.slave_run_triggers_for_rbr= @old_slave_run_triggers_for_rbr; +drop table t2; + +--echo # +--echo # MDEV-5513: Trigger is applied to the rows after first one +--echo # + +--connection master +create table t1 (a int, b int); +create table tlog (a int); +set sql_log_bin=0; +create trigger tr1 after insert on t1 for each row insert into tlog values (1); +set sql_log_bin=1; + +sync_slave_with_master; +--connection slave + +set @slave_run_triggers_for_rbr.saved = @@slave_run_triggers_for_rbr; +set global slave_run_triggers_for_rbr=1; +create trigger tr2 before insert on t1 for each row set new.b = new.a; + +--connection master +insert into t1 values (1,10),(2,20),(3,30); + +--sync_slave_with_master +select * from t1; + +--echo # +--echo # Verify slave skips running triggers if master ran and logged the row events for triggers +--echo # +--connection master +create table t4(a int, b int); +delete from tlog; +create trigger tr4 before insert on t4 for each row insert into tlog values (1); +insert into t4 values (1, 10),(2, 20); + +--sync_slave_with_master +select * from t4; +select * from tlog; + +# Cleanup +set global slave_run_triggers_for_rbr = @slave_run_triggers_for_rbr.saved; +--connection master +drop table t1, tlog, t4; +sync_slave_with_master; + +--source include/rpl_end.inc diff --git a/mysql-test/suite/rpl/t/rpl_row_triggers_sbr.test b/mysql-test/suite/rpl/t/rpl_row_triggers_sbr.test new file mode 100644 index 000000000000..20451f9fdb38 --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_row_triggers_sbr.test @@ -0,0 +1,37 @@ +--source include/have_binlog_format_statement.inc +--source include/have_rbr_triggers.inc +--source include/master-slave.inc + +--disable_query_log +CALL mtr.add_suppression("Cannot execute statement: impossible to write to binary log since statement is in row format and BINLOG_FORMAT = STATEMENT"); +--enable_query_log + +set binlog_format = row; +create table t1 (i int); +create table t2 (i int); + +--sync_slave_with_master + +--disable_query_log +CALL mtr.add_suppression("impossible to write to binary log since statement is in row format and BINLOG_FORMAT = STATEMENT"); +--enable_query_log + +SET @old_slave_run_triggers_for_rbr= @@global.slave_run_triggers_for_rbr; +set global slave_run_triggers_for_rbr=YES; + +create trigger tr_before before insert on t1 for each row + insert into t2 values (1); + +--connection master +insert into t1 values (1); +--connection slave +--let $slave_sql_errno= 1666 +--source include/wait_for_slave_sql_error_and_skip.inc + +--connection master +drop tables t1,t2; +--sync_slave_with_master + +SET @@global.slave_run_triggers_for_rbr= @old_slave_run_triggers_for_rbr; +--connection master +--source include/rpl_end.inc diff --git a/mysql-test/suite/sys_vars/r/slave_run_triggers_for_rbr_basic.result b/mysql-test/suite/sys_vars/r/slave_run_triggers_for_rbr_basic.result new file mode 100644 index 000000000000..488196a3c8f0 --- /dev/null +++ b/mysql-test/suite/sys_vars/r/slave_run_triggers_for_rbr_basic.result @@ -0,0 +1,27 @@ +set @old_slave_run_triggers_for_rbr = @@global.slave_run_triggers_for_rbr; +select @@global.slave_run_triggers_for_rbr; +@@global.slave_run_triggers_for_rbr +NO +set global slave_run_triggers_for_rbr = 1; +select @@global.slave_run_triggers_for_rbr; +@@global.slave_run_triggers_for_rbr +YES +set global slave_run_triggers_for_rbr = LOGGING; +select @@global.slave_run_triggers_for_rbr; +@@global.slave_run_triggers_for_rbr +LOGGING +set global slave_run_triggers_for_rbr = YES; +select @@global.slave_run_triggers_for_rbr; +@@global.slave_run_triggers_for_rbr +YES +set global slave_run_triggers_for_rbr = NO; +select @@global.slave_run_triggers_for_rbr; +@@global.slave_run_triggers_for_rbr +NO +set global slave_run_triggers_for_rbr = 100; +ERROR 42000: Variable 'slave_run_triggers_for_rbr' can't be set to the value of '100' +set global slave_run_triggers_for_rbr = WRONG_VALUE; +ERROR 42000: Variable 'slave_run_triggers_for_rbr' can't be set to the value of 'WRONG_VALUE' +set session slave_run_triggers_for_rbr = 1; +ERROR HY000: Variable 'slave_run_triggers_for_rbr' is a GLOBAL variable and should be set with SET GLOBAL +set @@global.slave_run_triggers_for_rbr = @old_slave_run_triggers_for_rbr; diff --git a/mysql-test/suite/sys_vars/t/slave_run_triggers_for_rbr_basic.test b/mysql-test/suite/sys_vars/t/slave_run_triggers_for_rbr_basic.test new file mode 100644 index 000000000000..6ed2b8f11563 --- /dev/null +++ b/mysql-test/suite/sys_vars/t/slave_run_triggers_for_rbr_basic.test @@ -0,0 +1,23 @@ +--source include/not_embedded.inc + +set @old_slave_run_triggers_for_rbr = @@global.slave_run_triggers_for_rbr; + +select @@global.slave_run_triggers_for_rbr; +set global slave_run_triggers_for_rbr = 1; +select @@global.slave_run_triggers_for_rbr; +set global slave_run_triggers_for_rbr = LOGGING; +select @@global.slave_run_triggers_for_rbr; +set global slave_run_triggers_for_rbr = YES; +select @@global.slave_run_triggers_for_rbr; +set global slave_run_triggers_for_rbr = NO; +select @@global.slave_run_triggers_for_rbr; + +--error ER_WRONG_VALUE_FOR_VAR +set global slave_run_triggers_for_rbr = 100; +--error ER_WRONG_VALUE_FOR_VAR +set global slave_run_triggers_for_rbr = WRONG_VALUE; + +--error ER_GLOBAL_VARIABLE +set session slave_run_triggers_for_rbr = 1; + +set @@global.slave_run_triggers_for_rbr = @old_slave_run_triggers_for_rbr; diff --git a/sql/log_event.cc b/sql/log_event.cc index 62f99e1b1870..b560db95bcd4 100644 --- a/sql/log_event.cc +++ b/sql/log_event.cc @@ -9612,6 +9612,27 @@ void check_extra_data(uchar* extra_row_data) Rows_log_event member functions **************************************************************************/ +#if !defined(MYSQL_CLIENT) && defined(HAVE_REPLICATION) +bool Rows_log_event::process_triggers(trg_event_type event, + trg_action_time_type time_type, + bool old_row_is_record1) +{ + bool result; + DBUG_ENTER("Rows_log_event::process_triggers"); + if (slave_run_triggers_for_rbr == SLAVE_RUN_TRIGGERS_FOR_RBR_YES) + { + tmp_disable_binlog(thd); /* Do not replicate the low-level changes. */ + result = m_table->triggers->process_triggers(thd, event, + time_type, old_row_is_record1); + reenable_binlog(thd); + } + else + result = m_table->triggers->process_triggers(thd, event, + time_type, old_row_is_record1); + DBUG_RETURN(result); +} +#endif + #ifndef MYSQL_CLIENT Rows_log_event::Rows_log_event(THD *thd_arg, TABLE *tbl_arg, const Table_id& tid, MY_BITMAP const *cols, bool using_trans, @@ -9629,7 +9650,8 @@ Rows_log_event::Rows_log_event(THD *thd_arg, TABLE *tbl_arg, const Table_id& tid m_type(event_type), m_extra_row_data(0) #ifdef HAVE_REPLICATION , m_curr_row(NULL), m_curr_row_end(NULL), m_key(NULL), m_key_info(NULL), - m_distinct_keys(Key_compare(&m_key_info)), m_distinct_key_spare_buf(NULL) + m_distinct_keys(Key_compare(&m_key_info)), m_distinct_key_spare_buf(NULL), + master_had_triggers(0) #endif { DBUG_ASSERT(tbl_arg && tbl_arg->s && tid.is_valid()); @@ -9692,7 +9714,8 @@ Rows_log_event::Rows_log_event(const char *buf, uint event_len, m_extra_row_data(0) #if !defined(MYSQL_CLIENT) && defined(HAVE_REPLICATION) , m_curr_row(NULL), m_curr_row_end(NULL), m_key(NULL), m_key_info(NULL), - m_distinct_keys(Key_compare(&m_key_info)), m_distinct_key_spare_buf(NULL) + m_distinct_keys(Key_compare(&m_key_info)), m_distinct_key_spare_buf(NULL), + master_had_triggers(0) #endif { DBUG_ENTER("Rows_log_event::Rows_log_event(const char*,...)"); @@ -11373,10 +11396,26 @@ int Rows_log_event::do_table_scan_and_update(Relay_log_info const *rli) DBUG_RETURN(error); } +/** + Restores empty table list as it was before trigger processing + + @note We have a lot of ASSERTS that check the lists when we close tables + There was the same problem with MERGE MYISAM tables and so here we try to + go the same way +*/ +static void restore_empty_query_table_list(LEX *lex) +{ + if (lex->first_not_own_table()) + (*lex->first_not_own_table()->prev_global)= NULL; + lex->query_tables= NULL; + lex->query_tables_last= &lex->query_tables; +} + int Rows_log_event::do_apply_event(Relay_log_info const *rli) { DBUG_ENTER("Rows_log_event::do_apply_event(Relay_log_info*)"); int error= 0; + TABLE *table; /* Estimate upon the size (end) of the first row. */ ulong estimated_rows= 0; @@ -11452,6 +11491,28 @@ int Rows_log_event::do_apply_event(Relay_log_info const *rli) /* A small test to verify that objects have consistent types */ DBUG_ASSERT(sizeof(thd->variables.option_bits) == sizeof(OPTION_RELAXED_UNIQUE_CHECKS)); + if (slave_run_triggers_for_rbr) + { + LEX *lex= thd->lex; + uint8 new_trg_event_map= get_trg_event_map(); + + /* + Trigger's procedures work with global table list. So we have to add + rli->tables_to_lock content there to get trigger's in the list + + Then restore_empty_query_table_list() restore the list as it was + */ + DBUG_ASSERT(lex->query_tables == NULL); + if ((lex->query_tables= rli->tables_to_lock)) + rli->tables_to_lock->prev_global= &lex->query_tables; + for (TABLE_LIST *tables= rli->tables_to_lock; tables; + tables= tables->next_global) + { + tables->trg_event_map= new_trg_event_map; + lex->query_tables_last= &tables->next_global; + } + } + if (open_and_lock_tables(thd, rli->tables_to_lock, FALSE, 0)) { uint actual_error= thd->get_stmt_da()->sql_errno(); @@ -11469,8 +11530,9 @@ int Rows_log_event::do_apply_event(Relay_log_info const *rli) "unexpected success or fatal error")); thd->is_slave_error= 1; } - const_cast(rli)->slave_close_thread_tables(thd); - DBUG_RETURN(actual_error); + error = actual_error; + /* remove trigger's tables */ + goto err; } /* @@ -11520,8 +11582,9 @@ int Rows_log_event::do_apply_event(Relay_log_info const *rli) having severe errors which should not be skiped. */ thd->is_slave_error= 1; - const_cast(rli)->slave_close_thread_tables(thd); - DBUG_RETURN(ERR_BAD_TABLE_DEF); + error= ERR_BAD_TABLE_DEF; + /* remove trigger's tables */ + goto err; } DBUG_PRINT("debug", ("Table: %s.%s is compatible with master" " - conv_table: %p", @@ -11547,19 +11610,29 @@ int Rows_log_event::do_apply_event(Relay_log_info const *rli) */ TABLE_LIST *ptr= rli->tables_to_lock; for (uint i=0 ; ptr && (i < rli->tables_to_lock_count); ptr= ptr->next_global, i++) + { const_cast(rli)->m_table_map.set_table(ptr->table_id, ptr->table); + /* + Following is passing flag about triggers on the server. The problem was + to pass it between table map event and row event. I do it via extended + TABLE_LIST (RPL_TABLE_LIST) but row event uses only TABLE so I need to + find somehow the corresponding TABLE_LIST. + */ + ptr->table->master_had_triggers = + ((RPL_TABLE_LIST*)ptr)->master_had_triggers; + } #ifdef HAVE_QUERY_CACHE query_cache.invalidate_locked_for_write(rli->tables_to_lock); #endif } - TABLE* - table= - m_table= const_cast(rli)->m_table_map.get_table(m_table_id); + table = m_table = + const_cast(rli)->m_table_map.get_table(m_table_id); - DBUG_PRINT("debug", ("m_table: 0x%lx, m_table_id: %llu", (ulong) m_table, - m_table_id.id())); + DBUG_PRINT("debug", ("m_table: 0x%lx, m_table_id: %llu%s", (ulong) m_table, + m_table_id.id(), (table && master_had_triggers ? + " (master had triggers)" : ""))); /* A row event comprising of a P_S table @@ -11571,6 +11644,7 @@ int Rows_log_event::do_apply_event(Relay_log_info const *rli) if (table) { + master_had_triggers = table->master_had_triggers; /* table == NULL means that this table should not be replicated (this was set up by Table_map_log_event::do_apply_event() @@ -11825,9 +11899,14 @@ int Rows_log_event::do_apply_event(Relay_log_info const *rli) */ thd->reset_current_stmt_binlog_format_row(); thd->is_slave_error= 1; - DBUG_RETURN(error); + /* remove trigger's tables */ + goto err; } + /* remove trigger's tables */ + if (slave_run_triggers_for_rbr) + restore_empty_query_table_list(thd->lex); + if (get_flags(STMT_END_F)) { if((error= rows_event_stmt_cleanup(rli, thd))) @@ -11849,6 +11928,12 @@ int Rows_log_event::do_apply_event(Relay_log_info const *rli) free_root(thd->mem_root, MYF(MY_KEEP_PREALLOC)); } DBUG_RETURN(error); + +err: + if (slave_run_triggers_for_rbr) + restore_empty_query_table_list(thd->lex); + const_cast(rli)->slave_close_thread_tables(thd); + DBUG_RETURN(error); } Log_event::enum_skip_reason @@ -12216,6 +12301,8 @@ Table_map_log_event::Table_map_log_event(THD *thd, TABLE *tbl, (tbl->s->db.str[tbl->s->db.length] == 0)); DBUG_ASSERT(tbl->s->table_name.str[tbl->s->table_name.length] == 0); + if (tbl->triggers) + m_flags |= TM_BIT_HAS_TRIGGERS_F; m_data_size= TABLE_MAP_HEADER_LEN; DBUG_EXECUTE_IF("old_row_based_repl_4_byte_map_id_master", m_data_size= 6;); @@ -12600,8 +12687,11 @@ int Table_map_log_event::do_apply_event(Relay_log_info const *rli) DBUG_EVALUATE_IF("inject_tblmap_same_id_maps_diff_table", 0, m_table_id.id()); table_list->updating= 1; table_list->required_type= FRMTYPE_TABLE; - DBUG_PRINT("debug", ("table: %s is mapped to %llu", table_list->table_name, - table_list->table_id.id())); + table_list->master_had_triggers = ((m_flags & TM_BIT_HAS_TRIGGERS_F) ? 1 : 0); + DBUG_PRINT("debug", ("table: %s is mapped to %llu%s", table_list->table_name, + table_list->table_id.id(), + (table_list->master_had_triggers ? + " (master had triggers)" : ""))); enum_tbl_map_status tblmap_status= check_table_map(rli, table_list); if (tblmap_status == OK_TO_PROCESS) @@ -12879,6 +12969,8 @@ Write_rows_log_event::do_before_row_operations(const Slave_reporting_capability /* NDB specific: update from ndb master wrapped as Write_rows so that the event should be applied to replace slave's row + + Also following is needed in case if we have AFTER DELETE triggers */ m_table->file->extra(HA_EXTRA_WRITE_CAN_REPLACE); /* @@ -12894,7 +12986,9 @@ Write_rows_log_event::do_before_row_operations(const Slave_reporting_capability */ } - + if (slave_run_triggers_for_rbr && !master_had_triggers && m_table->triggers ) + m_table->prepare_triggers_for_insert_stmt_or_event(); + /* Honor next number column if present */ m_table->next_number_field= m_table->found_next_number_field; /* @@ -13064,6 +13158,9 @@ Write_rows_log_event::write_row(const Relay_log_info *const rli, int UNINIT_VAR(keynum); auto_afree_ptr key(NULL); + const bool invoke_triggers = + slave_run_triggers_for_rbr && !master_had_triggers && table->triggers; + prepare_record(table, &m_cols, table->file->ht->db_type != DB_TYPE_NDBCLUSTER); @@ -13071,8 +13168,9 @@ Write_rows_log_event::write_row(const Relay_log_info *const rli, if ((error= unpack_current_row(rli, &m_cols))) DBUG_RETURN(error); - if (m_curr_row == m_rows_buf) + if (m_curr_row == m_rows_buf && !invoke_triggers) { + // This table has no triggers so we can do bulk insert /* this is the first row to be inserted, we estimate the rows with the size of the first row and use that value to initialize storage engine for bulk insertion */ @@ -13099,6 +13197,10 @@ Write_rows_log_event::write_row(const Relay_log_info *const rli, DBUG_PRINT_BITSET("debug", "read_set = %s", table->read_set); #endif + if (invoke_triggers && + process_triggers(TRG_EVENT_INSERT, TRG_ACTION_BEFORE, TRUE)) + DBUG_RETURN(HA_ERR_GENERIC); // in case if error is not set yet + /* Try to write record. If a corresponding record already exists in the table, we try to change it using ha_update_row() if possible. Otherwise we delete @@ -13243,38 +13345,66 @@ Write_rows_log_event::write_row(const Relay_log_info *const rli, !table->file->referenced_by_foreign_key()) { DBUG_PRINT("info",("Updating row using ha_update_row()")); - error=table->file->ha_update_row(table->record[1], - table->record[0]); - switch (error) { + if (invoke_triggers && + process_triggers(TRG_EVENT_UPDATE, TRG_ACTION_BEFORE, FALSE)) + error= HA_ERR_GENERIC; // in case if error is not set yet + else + { + error=table->file->ha_update_row(table->record[1], + table->record[0]); + switch (error) { - case HA_ERR_RECORD_IS_THE_SAME: - DBUG_PRINT("info",("ignoring HA_ERR_RECORD_IS_THE_SAME error from" - " ha_update_row()")); - error= 0; + case HA_ERR_RECORD_IS_THE_SAME: + DBUG_PRINT("info",("ignoring HA_ERR_RECORD_IS_THE_SAME error from" + " ha_update_row()")); + error= 0; - case 0: - break; + case 0: + break; - default: - DBUG_PRINT("info",("ha_update_row() returns error %d",error)); - table->file->print_error(error, MYF(0)); + default: + DBUG_PRINT("info",("ha_update_row() returns error %d",error)); + table->file->print_error(error, MYF(0)); + } + if (invoke_triggers && !error && + (process_triggers(TRG_EVENT_UPDATE, TRG_ACTION_AFTER, TRUE) || + process_triggers(TRG_EVENT_INSERT, TRG_ACTION_AFTER, TRUE))) + error= HA_ERR_GENERIC; // in case if error is not set yet } - goto error; } else { DBUG_PRINT("info",("Deleting offending row and trying to write new one again")); - if ((error= table->file->ha_delete_row(table->record[1]))) + if (invoke_triggers && + process_triggers(TRG_EVENT_DELETE, TRG_ACTION_BEFORE, TRUE)) { - DBUG_PRINT("info",("ha_delete_row() returns error %d",error)); - table->file->print_error(error, MYF(0)); + error = HA_ERR_GENERIC; // in case if error is not set yet goto error; } + else + { + if ((error= table->file->ha_delete_row(table->record[1]))) + { + DBUG_PRINT("info",("ha_delete_row() returns error %d",error)); + table->file->print_error(error, MYF(0)); + goto error; + } + if (invoke_triggers && + process_triggers(TRG_EVENT_DELETE, TRG_ACTION_AFTER, TRUE)) + { + error = HA_ERR_GENERIC; // in case if error is not set yet + goto error; + } + } /* Will retry ha_write_row() with the offending row removed. */ } } + if (invoke_triggers && + process_triggers(TRG_EVENT_INSERT, TRG_ACTION_AFTER, TRUE)) + error= HA_ERR_GENERIC; // in case if error is not set yet + error: m_table->default_column_bitmaps(); DBUG_RETURN(error); @@ -13308,6 +13438,15 @@ void Write_rows_log_event::print(FILE *file, PRINT_EVENT_INFO* print_event_info) } #endif +#if defined(MYSQL_SERVER) && defined(HAVE_REPLICATION) +uint8 Write_rows_log_event::get_trg_event_map() +{ + return (static_cast (1 << static_cast(TRG_EVENT_INSERT)) | + static_cast (1 << static_cast(TRG_EVENT_UPDATE)) | + static_cast (1 << static_cast(TRG_EVENT_DELETE))); +} +#endif + /************************************************************************** Delete_rows_log_event member functions **************************************************************************/ @@ -13354,6 +13493,8 @@ Delete_rows_log_event::do_before_row_operations(const Slave_reporting_capability */ if (get_flags(STMT_END_F)) status_var_increment(thd->status_var.com_stat[SQLCOM_DELETE]); + if (slave_run_triggers_for_rbr && !master_had_triggers) + m_table->prepare_triggers_for_delete_stmt_or_event(); error= row_operations_scan_and_key_setup(); DBUG_RETURN(error); @@ -13370,11 +13511,20 @@ Delete_rows_log_event::do_after_row_operations(const Slave_reporting_capability int Delete_rows_log_event::do_exec_row(const Relay_log_info *const rli) { - int error; + const bool invoke_triggers = + slave_run_triggers_for_rbr && !master_had_triggers && m_table->triggers; + int error = 0; DBUG_ASSERT(m_table != NULL); /* m_table->record[0] contains the BI */ m_table->mark_columns_per_binlog_row_image(); - error= m_table->file->ha_delete_row(m_table->record[0]); + if (invoke_triggers && + process_triggers(TRG_EVENT_DELETE, TRG_ACTION_BEFORE, FALSE)) + error= HA_ERR_GENERIC; // in case if error is not set yet + if (!error) + error= m_table->file->ha_delete_row(m_table->record[0]); + if (invoke_triggers && !error && + process_triggers(TRG_EVENT_DELETE, TRG_ACTION_AFTER, FALSE)) + error= HA_ERR_GENERIC; // in case if error is not set yet m_table->default_column_bitmaps(); return error; } @@ -13389,7 +13539,12 @@ void Delete_rows_log_event::print(FILE *file, } #endif - +#if defined(MYSQL_SERVER) && defined(HAVE_REPLICATION) +uint8 Delete_rows_log_event::get_trg_event_map() +{ + return static_cast (1 << static_cast(TRG_EVENT_DELETE)); +} +#endif /************************************************************************** Update_rows_log_event member functions **************************************************************************/ @@ -13463,6 +13618,9 @@ Update_rows_log_event::do_before_row_operations(const Slave_reporting_capability */ if (get_flags(STMT_END_F)) status_var_increment(thd->status_var.com_stat[SQLCOM_UPDATE]); + if (slave_run_triggers_for_rbr && !master_had_triggers) + m_table->prepare_triggers_for_update_stmt_or_event(); + error= row_operations_scan_and_key_setup(); DBUG_RETURN(error); @@ -13480,6 +13638,9 @@ Update_rows_log_event::do_after_row_operations(const Slave_reporting_capability int Update_rows_log_event::do_exec_row(const Relay_log_info *const rli) { + const bool invoke_triggers = + slave_run_triggers_for_rbr && !master_had_triggers && m_table->triggers; + DBUG_ASSERT(m_table != NULL); int error= 0; @@ -13520,9 +13681,20 @@ Update_rows_log_event::do_exec_row(const Relay_log_info *const rli) memcpy(m_table->write_set->bitmap, m_cols_ai.bitmap, (m_table->write_set->n_bits + 7) / 8); m_table->mark_columns_per_binlog_row_image(); + if (invoke_triggers && + process_triggers(TRG_EVENT_UPDATE, TRG_ACTION_BEFORE, TRUE)) + { + error= HA_ERR_GENERIC; // in case if error is not set yet + goto err; + } error= m_table->file->ha_update_row(m_table->record[1], m_table->record[0]); if (error == HA_ERR_RECORD_IS_THE_SAME) error= 0; + if (invoke_triggers && !error && + process_triggers(TRG_EVENT_UPDATE, TRG_ACTION_AFTER, TRUE)) + error= HA_ERR_GENERIC; // in case if error is not set yet + +err: m_table->default_column_bitmaps(); return error; @@ -13582,6 +13754,12 @@ Incident_log_event::Incident_log_event(const char *buf, uint event_len, DBUG_VOID_RETURN; } +#if defined(MYSQL_SERVER) && defined(HAVE_REPLICATION) +uint8 Update_rows_log_event::get_trg_event_map() +{ + return static_cast (1 << static_cast(TRG_EVENT_UPDATE)); +} +#endif Incident_log_event::~Incident_log_event() { diff --git a/sql/log_event.h b/sql/log_event.h index 3734680d86c5..74cb77309956 100644 --- a/sql/log_event.h +++ b/sql/log_event.h @@ -3914,7 +3914,9 @@ class Table_map_log_event : public Log_event { TM_NO_FLAGS = 0U, TM_BIT_LEN_EXACT_F = (1U << 0), - TM_REFERRED_FK_DB_F = (1U << 1) + TM_REFERRED_FK_DB_F = (1U << 1), + // MariaDB flags (we starts from the other end) + TM_BIT_HAS_TRIGGERS_F = (1U << 14) }; flag_set get_flags(flag_set flag) const { return m_flags & flag; } @@ -4206,6 +4208,10 @@ class Rows_log_event : public Log_event const uchar* get_extra_row_data() const { return m_extra_row_data; } +#if defined(MYSQL_SERVER) && defined(HAVE_REPLICATION) + virtual uint8 get_trg_event_map() = 0; +#endif + protected: /* The constructors are protected since you're supposed to inherit @@ -4312,6 +4318,7 @@ class Rows_log_event : public Log_event for doing an index scan with HASH_SCAN search algorithm. */ uchar *m_distinct_key_spare_buf; + bool master_had_triggers; // Unpack the current row into m_table->record[0] int unpack_current_row(const Relay_log_info *const rli, @@ -4526,6 +4533,10 @@ class Rows_log_event : public Log_event @returns 0 on success. Otherwise, the error code. */ int do_scan_and_update(Relay_log_info const *rli); +public: + bool process_triggers(trg_event_type event, + trg_action_time_type time_type, + bool old_row_is_record1); #endif /* defined(MYSQL_SERVER) && defined(HAVE_REPLICATION) */ friend class Old_rows_log_event; @@ -4589,6 +4600,7 @@ class Write_rows_log_event : public Rows_log_event virtual int do_before_row_operations(const Slave_reporting_capability *const); virtual int do_after_row_operations(const Slave_reporting_capability *const,int); virtual int do_exec_row(const Relay_log_info *const); + uint8 get_trg_event_map(); #endif }; @@ -4662,6 +4674,7 @@ class Update_rows_log_event : public Rows_log_event virtual int do_before_row_operations(const Slave_reporting_capability *const); virtual int do_after_row_operations(const Slave_reporting_capability *const,int); virtual int do_exec_row(const Relay_log_info *const); + uint8 get_trg_event_map(); #endif /* defined(MYSQL_SERVER) && defined(HAVE_REPLICATION) */ }; @@ -4725,6 +4738,7 @@ class Delete_rows_log_event : public Rows_log_event virtual int do_before_row_operations(const Slave_reporting_capability *const); virtual int do_after_row_operations(const Slave_reporting_capability *const,int); virtual int do_exec_row(const Relay_log_info *const); + uint8 get_trg_event_map(); #endif }; diff --git a/sql/mysqld.cc b/sql/mysqld.cc index f25a1a77d8c4..ea562f0b83c5 100644 --- a/sql/mysqld.cc +++ b/sql/mysqld.cc @@ -581,6 +581,7 @@ ulong open_files_limit, max_binlog_size, max_relay_log_size; ulong slave_trans_retries; uint slave_net_timeout; ulong slave_exec_mode_options; +ulong slave_run_triggers_for_rbr = 0; ulonglong slave_type_conversions_options; ulong opt_mts_slave_parallel_workers; ulonglong opt_mts_pending_jobs_size_max; diff --git a/sql/mysqld.h b/sql/mysqld.h index 2dfc13d34e09..418b75f9be31 100644 --- a/sql/mysqld.h +++ b/sql/mysqld.h @@ -281,6 +281,7 @@ extern my_bool opt_safe_user_create; extern my_bool opt_safe_show_db, opt_local_infile, opt_myisam_use_mmap; extern my_bool opt_slave_compressed_protocol, use_temp_pool; extern ulong slave_exec_mode_options; +extern ulong slave_run_triggers_for_rbr; extern ulonglong slave_type_conversions_options; extern my_bool read_only, opt_readonly, super_read_only, opt_super_readonly; extern my_bool allow_document_type; diff --git a/sql/rpl_utility.h b/sql/rpl_utility.h index e2c37160343b..9e66a27ec825 100644 --- a/sql/rpl_utility.h +++ b/sql/rpl_utility.h @@ -427,6 +427,7 @@ struct RPL_TABLE_LIST bool m_tabledef_valid; table_def m_tabledef; TABLE *m_conv_table; + bool master_had_triggers; }; diff --git a/sql/sql_class.h b/sql/sql_class.h index 62f0c2dfc69d..c7e1e6da159c 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -98,6 +98,11 @@ enum enum_delay_key_write { DELAY_KEY_WRITE_NONE, DELAY_KEY_WRITE_ON, enum enum_slave_exec_mode { SLAVE_EXEC_MODE_STRICT, SLAVE_EXEC_MODE_IDEMPOTENT, SLAVE_EXEC_MODE_LAST_BIT }; + +enum enum_slave_run_triggers_for_rbr { SLAVE_RUN_TRIGGERS_FOR_RBR_NO, + SLAVE_RUN_TRIGGERS_FOR_RBR_YES, + SLAVE_RUN_TRIGGERS_FOR_RBR_LOGGING}; + enum enum_slave_type_conversions { SLAVE_TYPE_CONVERSIONS_ALL_LOSSY, SLAVE_TYPE_CONVERSIONS_ALL_NON_LOSSY, SLAVE_TYPE_CONVERSIONS_ALL_UNSIGNED, diff --git a/sql/sql_delete.cc b/sql/sql_delete.cc index f3e887641925..c53c4e4980ce 100644 --- a/sql/sql_delete.cc +++ b/sql/sql_delete.cc @@ -332,16 +332,8 @@ bool mysql_delete(THD *thd, TABLE_LIST *table_list, Item *conds, init_ftfuncs(thd, select_lex, 1); THD_STAGE_INFO(thd, stage_updating); - if (table->triggers && - table->triggers->has_triggers(TRG_EVENT_DELETE, - TRG_ACTION_AFTER)) + if (table->prepare_triggers_for_delete_stmt_or_event()) { - /* - The table has AFTER DELETE triggers that might access to subject table - and therefore might need delete to be done immediately. So we turn-off - the batching. - */ - (void) table->file->extra(HA_EXTRA_DELETE_CANNOT_BATCH); will_batch= FALSE; } else @@ -720,17 +712,7 @@ multi_delete::initialize_tables(JOIN *join) transactional_tables= 1; else normal_tables= 1; - if (tbl->triggers && - tbl->triggers->has_triggers(TRG_EVENT_DELETE, - TRG_ACTION_AFTER)) - { - /* - The table has AFTER DELETE triggers that might access to subject - table and therefore might need delete to be done immediately. - So we turn-off the batching. - */ - (void) tbl->file->extra(HA_EXTRA_DELETE_CANNOT_BATCH); - } + tbl->prepare_triggers_for_delete_stmt_or_event(); tbl->prepare_for_position(); tbl->mark_columns_needed_for_delete(); } diff --git a/sql/sql_insert.cc b/sql/sql_insert.cc index a32c76b4eb65..c16b23717b28 100644 --- a/sql/sql_insert.cc +++ b/sql/sql_insert.cc @@ -360,48 +360,6 @@ bool validate_default_values_of_unset_fields(THD *thd, TABLE *table) } -/* - Prepare triggers for INSERT-like statement. - - SYNOPSIS - prepare_triggers_for_insert_stmt() - table Table to which insert will happen - - NOTE - Prepare triggers for INSERT-like statement by marking fields - used by triggers and inform handlers that batching of UPDATE/DELETE - cannot be done if there are BEFORE UPDATE/DELETE triggers. -*/ - -void prepare_triggers_for_insert_stmt(TABLE *table) -{ - if (table->triggers) - { - if (table->triggers->has_triggers(TRG_EVENT_DELETE, - TRG_ACTION_AFTER)) - { - /* - The table has AFTER DELETE triggers that might access to - subject table and therefore might need delete to be done - immediately. So we turn-off the batching. - */ - (void) table->file->extra(HA_EXTRA_DELETE_CANNOT_BATCH); - } - if (table->triggers->has_triggers(TRG_EVENT_UPDATE, - TRG_ACTION_AFTER)) - { - /* - The table has AFTER UPDATE triggers that might access to subject - table and therefore might need update to be done immediately. - So we turn-off the batching. - */ - (void) table->file->extra(HA_EXTRA_UPDATE_CANNOT_BATCH); - } - } - table->mark_columns_needed_for_insert(); -} - - /** Upgrade table-level lock of INSERT statement to TL_WRITE if a more concurrent lock is infeasible for some reason. This is @@ -970,8 +928,8 @@ bool mysql_insert(THD *thd,TABLE_LIST *table_list, thd->abort_on_warning= (!ignore && thd->is_strict_mode()); - prepare_triggers_for_insert_stmt(table); - + table->prepare_triggers_for_insert_stmt_or_event(); + table->mark_columns_needed_for_insert(); if (table_list->prepare_where(thd, 0, TRUE) || table_list->prepare_check_option(thd)) @@ -3613,7 +3571,10 @@ select_insert::prepare(List &values, SELECT_LEX_UNIT *u) table_list->prepare_check_option(thd)); if (!res) - prepare_triggers_for_insert_stmt(table); + { + table->prepare_triggers_for_insert_stmt_or_event(); + table->mark_columns_needed_for_insert(); + } DBUG_RETURN(res); } diff --git a/sql/sql_insert.h b/sql/sql_insert.h index f0dd1e54b413..4934f2624173 100644 --- a/sql/sql_insert.h +++ b/sql/sql_insert.h @@ -37,7 +37,6 @@ void upgrade_lock_type_for_insert(THD *thd, thr_lock_type *lock_type, bool is_multi_insert); int check_that_all_fields_are_given_values(THD *thd, TABLE *entry, TABLE_LIST *table_list); -void prepare_triggers_for_insert_stmt(TABLE *table); int write_record(THD *thd, TABLE *table, COPY_INFO *info, COPY_INFO *update); void kill_delayed_threads(void); diff --git a/sql/sql_load.cc b/sql/sql_load.cc index e49d42d1776b..241723b43729 100644 --- a/sql/sql_load.cc +++ b/sql/sql_load.cc @@ -27,7 +27,6 @@ #include #include "sql_view.h" // check_key_in_view #include "sql_insert.h" // check_that_all_fields_are_given_values, - // prepare_triggers_for_insert_stmt, // write_record #include "sql_acl.h" // INSERT_ACL, UPDATE_ACL #include "log_event.h" // Delete_file_log_event, @@ -340,7 +339,8 @@ int mysql_load(THD *thd,sql_exchange *ex,TABLE_LIST *table_list, if (info.add_function_default_columns(table, table->write_set)) DBUG_RETURN(TRUE); - prepare_triggers_for_insert_stmt(table); + table->prepare_triggers_for_insert_stmt_or_event(); + table->mark_columns_needed_for_insert(); uint tot_length=0; bool use_blobs= 0, use_vars= 0; diff --git a/sql/sql_update.cc b/sql/sql_update.cc index bc54a6a73781..901366594535 100644 --- a/sql/sql_update.cc +++ b/sql/sql_update.cc @@ -717,16 +717,8 @@ int mysql_update(THD *thd, transactional_table= table->file->has_transactions(); thd->abort_on_warning= (!ignore && thd->is_strict_mode()); - if (table->triggers && - table->triggers->has_triggers(TRG_EVENT_UPDATE, - TRG_ACTION_AFTER)) + if (table->prepare_triggers_for_update_stmt_or_event()) { - /* - The table has AFTER UPDATE triggers that might access to subject - table and therefore might need update to be done immediately. - So we turn-off the batching. - */ - (void) table->file->extra(HA_EXTRA_UPDATE_CANNOT_BATCH); will_batch= FALSE; } else @@ -1642,17 +1634,7 @@ int multi_update::prepare(List ¬_used_values, table->no_keyread=1; table->covering_keys.clear_all(); table->pos_in_table_list= tl; - if (table->triggers && - table->triggers->has_triggers(TRG_EVENT_UPDATE, - TRG_ACTION_AFTER)) - { - /* - The table has AFTER UPDATE triggers that might access to subject - table and therefore might need update to be done immediately. - So we turn-off the batching. - */ - (void) table->file->extra(HA_EXTRA_UPDATE_CANNOT_BATCH); - } + table->prepare_triggers_for_update_stmt_or_event(); } } diff --git a/sql/sys_vars.cc b/sql/sys_vars.cc index 214b723788c6..01816a396ad8 100644 --- a/sql/sys_vars.cc +++ b/sql/sys_vars.cc @@ -2948,6 +2948,21 @@ static Sys_var_enum Slave_exec_mode( "between the master and the slave", GLOBAL_VAR(slave_exec_mode_options), CMD_LINE(REQUIRED_ARG), slave_exec_mode_names, DEFAULT(SLAVE_EXEC_MODE_STRICT)); +static const char *slave_run_triggers_for_rbr_names[]= + {"NO", "YES", "LOGGING", 0}; +static Sys_var_enum Slave_run_triggers_for_rbr( + "slave_run_triggers_for_rbr", + "Modes for how triggers in row-base replication on slave side will be " + "executed. Legal values are NO (default), YES and LOGGING. NO means " + "that trigger for RBR will not be running on slave. YES and LOGGING " + "means that triggers will be running on slave, if there was not " + "triggers running on the master for the statement. LOGGING also means " + "results of that the executed triggers work will be written to " + "the binlog.", + GLOBAL_VAR(slave_run_triggers_for_rbr), CMD_LINE(REQUIRED_ARG), + slave_run_triggers_for_rbr_names, + DEFAULT(SLAVE_RUN_TRIGGERS_FOR_RBR_NO)); + const char *slave_type_conversions_name[]= {"ALL_LOSSY", "ALL_NON_LOSSY", "ALL_UNSIGNED", "ALL_SIGNED", 0}; static Sys_var_set Slave_type_conversions( diff --git a/sql/table.cc b/sql/table.cc index 5bc6931dccea..2c7987b85d9a 100644 --- a/sql/table.cc +++ b/sql/table.cc @@ -3717,7 +3717,10 @@ void TABLE::init(THD *thd, TABLE_LIST *tl) fulltext_searched= 0; file->ft_handler= 0; reginfo.impossible_range= 0; - +#ifdef HAVE_REPLICATION + /* used in RBR Triggers */ + master_had_triggers = 0; +#endif /* Catch wrong handling of the auto_increment_field_not_null. */ DBUG_ASSERT(!auto_increment_field_not_null); auto_increment_field_not_null= FALSE; @@ -6465,3 +6468,73 @@ bool is_simple_order(ORDER *order) } return TRUE; } + +/* + Prepare triggers for INSERT-like statement. + SYNOPSIS + prepare_triggers_for_insert_stmt_or_event() + NOTE + Prepare triggers for INSERT-like statement by marking fields + used by triggers and inform handlers that batching of UPDATE/DELETE + cannot be done if there are BEFORE UPDATE/DELETE triggers. +*/ +void TABLE::prepare_triggers_for_insert_stmt_or_event() +{ + if (triggers) + { + if (triggers->has_triggers(TRG_EVENT_DELETE, + TRG_ACTION_AFTER)) + { + /* + The table has AFTER DELETE triggers that might access to + subject table and therefore might need delete to be done + immediately. So we turn-off the batching. + */ + (void) file->extra(HA_EXTRA_DELETE_CANNOT_BATCH); + } + if (triggers->has_triggers(TRG_EVENT_UPDATE, + TRG_ACTION_AFTER)) + { + /* + The table has AFTER UPDATE triggers that might access to subject + table and therefore might need update to be done immediately. + So we turn-off the batching. + */ + (void) file->extra(HA_EXTRA_UPDATE_CANNOT_BATCH); + } + } +} + +bool TABLE::prepare_triggers_for_delete_stmt_or_event() +{ + if (triggers && + triggers->has_triggers(TRG_EVENT_DELETE, + TRG_ACTION_AFTER)) + { + /* + The table has AFTER DELETE triggers that might access to subject table + and therefore might need delete to be done immediately. So we turn-off + the batching. + */ + (void) file->extra(HA_EXTRA_DELETE_CANNOT_BATCH); + return TRUE; + } + return FALSE; +} + +bool TABLE::prepare_triggers_for_update_stmt_or_event() +{ + if (triggers && + triggers->has_triggers(TRG_EVENT_UPDATE, + TRG_ACTION_AFTER)) + { + /* + The table has AFTER UPDATE triggers that might access to subject + table and therefore might need update to be done immediately. + So we turn-off the batching. + */ + (void) file->extra(HA_EXTRA_UPDATE_CANNOT_BATCH); + return TRUE; + } + return FALSE; +} diff --git a/sql/table.h b/sql/table.h index 48cf45c2033c..bfdd4eecb5a8 100644 --- a/sql/table.h +++ b/sql/table.h @@ -1213,6 +1213,10 @@ struct TABLE bool all_partitions_pruned_away; #endif MDL_ticket *mdl_ticket; +#ifdef HAVE_REPLICATION + /* used in RBR Triggers */ + bool master_had_triggers; +#endif void init(THD *thd, TABLE_LIST *tl); bool fill_item_list(List *item_list) const; @@ -1314,6 +1318,9 @@ struct TABLE { tmp_file_created= false; } + void prepare_triggers_for_insert_stmt_or_event(); + bool prepare_triggers_for_delete_stmt_or_event(); + bool prepare_triggers_for_update_stmt_or_event(); };