BUG#20141 "User-defined variables are not replicated properly for

          SF/Triggers in SBR mode."
BUG#14914 "SP: Uses of session variables in routines are not always replicated"
BUG#25167 "Dupl. usage of user-variables in trigger/function is not replicated
          correctly"

User-defined variables used inside of stored functions/triggers in
statements which did not update tables directly were not replicated.
We also had problems with replication of user-defined variables which
were used in triggers (or stored functions called from table-updating
statements) more than once.

This patch addresses the first issue by enabling logging of all
references to user-defined variables in triggers/stored functions
and not only references from table-updating statements.

The second issue stemmed from the fact that for user-defined
variables used from triggers or stored functions called from
table-updating statements we were writing binlog events for each
reference instead of only one event for the first reference.
This problem is already solved for stored functions called from
non-updating statements with help of "event unioning" mechanism.
So the patch simply extends this mechanism to the case affected.
It also fixes small problem in this mechanism which caused wrong
logging of references to user-variables in cases when non-updating
statement called several stored functions which used the same
variable and some of these function calls were omitted from binlog
as they were not updating any tables.
parent 54b04ff5
...@@ -80,4 +80,178 @@ abc\def ...@@ -80,4 +80,178 @@ abc\def
This is a test This is a test
insert into t1 select * FROM (select @var1 union select @var2) AS t2; insert into t1 select * FROM (select @var1 union select @var2) AS t2;
drop table t1; drop table t1;
End of 4.1 tests.
DROP TABLE IF EXISTS t20;
DROP TABLE IF EXISTS t21;
DROP PROCEDURE IF EXISTS test.insert;
CREATE TABLE t20 (a VARCHAR(20));
CREATE TABLE t21 (a VARCHAR(20));
CREATE PROCEDURE test.insert()
BEGIN
IF (@VAR)
THEN
INSERT INTO test.t20 VALUES ('SP_TRUE');
ELSE
INSERT INTO test.t20 VALUES ('SP_FALSE');
END IF;
END|
CREATE TRIGGER test.insert_bi BEFORE INSERT
ON test.t20 FOR EACH ROW
BEGIN
IF (@VAR)
THEN
INSERT INTO test.t21 VALUES ('TRIG_TRUE');
ELSE
INSERT INTO test.t21 VALUES ('TRIG_FALSE');
END IF;
END|
SET @VAR=0;
CALL test.insert();
SET @VAR=1;
CALL test.insert();
On master: Check the tables for correct data
SELECT * FROM t20;
a
SP_FALSE
SP_TRUE
SELECT * FROM t21;
a
TRIG_FALSE
TRIG_TRUE
On slave: Check the tables for correct data and it matches master
SELECT * FROM t20;
a
SP_FALSE
SP_TRUE
SELECT * FROM t21;
a
TRIG_FALSE
TRIG_TRUE
DROP TABLE t20;
DROP TABLE t21;
DROP PROCEDURE test.insert;
DROP TABLE IF EXISTS t1;
DROP FUNCTION IF EXISTS test.square;
CREATE TABLE t1 (i INT);
CREATE FUNCTION test.square() RETURNS INTEGER DETERMINISTIC RETURN
(@var * @var);
SET @var = 1;
INSERT INTO t1 VALUES (square());
SET @var = 2;
INSERT INTO t1 VALUES (square());
SET @var = 3;
INSERT INTO t1 VALUES (square());
SET @var = 4;
INSERT INTO t1 VALUES (square());
SET @var = 5;
INSERT INTO t1 VALUES (square());
On master: Retrieve the values from the table
SELECT * FROM t1;
i
1
4
9
16
25
On slave: Retrieve the values from the table and verify they are the same as on master
SELECT * FROM t1;
i
1
4
9
16
25
DROP TABLE t1;
DROP FUNCTION test.square;
DROP TABLE IF EXISTS t1;
DROP FUNCTION IF EXISTS f1;
DROP FUNCTION IF EXISTS f2;
CREATE TABLE t1(a int);
CREATE FUNCTION f1() returns int deterministic BEGIN
return @a;
END |
CREATE FUNCTION f2() returns int deterministic BEGIN
IF (@b > 0) then
SET @c = (@a + @b);
else
SET @c = (@a - 1);
END if;
return @c;
END |
SET @a=500;
INSERT INTO t1 values(f1());
SET @b = 125;
SET @c = 1;
INSERT INTO t1 values(f2());
On master: Retrieve the values from the table
SELECT * from t1;
a
500
625
On slave: Check the tables for correct data and it matches master
SELECT * from t1;
a
500
625
DROP TABLE t1;
DROP FUNCTION f1;
DROP FUNCTION f2;
DROP TABLE IF EXISTS t1;
DROP TABLE IF EXISTS t2;
CREATE TABLE t1 (i int);
CREATE TABLE t2 (k int);
CREATE trigger t1_bi before INSERT on t1 for each row BEGIN
INSERT INTO t2 values (@a);
SET @a:=42;
INSERT INTO t2 values (@a);
END |
SET @a:=100;
INSERT INTO t1 values (5);
On master: Check to see that data was inserted correctly in both tables
SELECT * from t1;
i
5
SELECT * from t2;
k
100
42
On slave: Check the tables for correct data and it matches master
SELECT * from t1;
i
5
SELECT * from t2;
k
100
42
End of 5.0 tests.
DROP TABLE t1;
DROP TABLE t2;
DROP TABLE IF EXISTS t1;
DROP FUNCTION IF EXISTS f1;
DROP FUNCTION IF EXISTS f2;
CREATE TABLE t1 (i INT);
CREATE FUNCTION f1() RETURNS INT RETURN @a;
CREATE
FUNCTION f2() RETURNS INT BEGIN
INSERT INTO t1 VALUES (10 + @a);
RETURN 0;
END|
SET @a:=123;
SELECT f1(), f2();
f1() f2()
123 0
On master: Check to see that data was inserted correctly
INSERT INTO t1 VALUES(f1());
SELECT * FROM t1;
i
133
123
On slave: Check the table for correct data and it matches master
SELECT * FROM t1;
i
133
123
DROP FUNCTION f1;
DROP FUNCTION f2;
DROP TABLE t1;
stop slave; stop slave;
...@@ -53,5 +53,302 @@ SELECT * FROM t1 ORDER BY n; ...@@ -53,5 +53,302 @@ SELECT * FROM t1 ORDER BY n;
connection master; connection master;
insert into t1 select * FROM (select @var1 union select @var2) AS t2; insert into t1 select * FROM (select @var1 union select @var2) AS t2;
drop table t1; drop table t1;
--echo End of 4.1 tests.
# BUG#20141
# The following tests ensure that if user-defined variables are used in SF/Triggers
# that they are replicated correctly. These tests should be run in both SBR and RBR
# modes.
# This test uses a procedure that inserts data values based on the value of a
# user-defined variable. It also has a trigger that inserts data based on the
# same variable. Successful test runs show that the @var is replicated
# properly and that the procedure and trigger insert the correct data on the
# slave.
#
# The test of stored procedure was included for completeness. Replication of stored
# procedures was not directly affected by BUG#20141.
#
# This test was constructed for BUG#20141
--disable_warnings
DROP TABLE IF EXISTS t20;
DROP TABLE IF EXISTS t21;
DROP PROCEDURE IF EXISTS test.insert;
--enable_warnings
CREATE TABLE t20 (a VARCHAR(20));
CREATE TABLE t21 (a VARCHAR(20));
DELIMITER |;
# Create a procedure that uses the @var for flow control
CREATE PROCEDURE test.insert()
BEGIN
IF (@VAR)
THEN
INSERT INTO test.t20 VALUES ('SP_TRUE');
ELSE
INSERT INTO test.t20 VALUES ('SP_FALSE');
END IF;
END|
# Create a trigger that uses the @var for flow control
CREATE TRIGGER test.insert_bi BEFORE INSERT
ON test.t20 FOR EACH ROW
BEGIN
IF (@VAR)
THEN
INSERT INTO test.t21 VALUES ('TRIG_TRUE');
ELSE
INSERT INTO test.t21 VALUES ('TRIG_FALSE');
END IF;
END|
DELIMITER ;|
sync_slave_with_master;
connection master;
# Set @var and call the procedure, repeat with different values
SET @VAR=0;
CALL test.insert();
SET @VAR=1;
CALL test.insert();
--echo On master: Check the tables for correct data
SELECT * FROM t20;
SELECT * FROM t21;
sync_slave_with_master;
--echo On slave: Check the tables for correct data and it matches master
SELECT * FROM t20;
SELECT * FROM t21;
connection master;
# Cleanup
DROP TABLE t20;
DROP TABLE t21;
DROP PROCEDURE test.insert;
# This test uses a stored function that uses user-defined variables to return data
# This test was constructed for BUG#20141
--disable_warnings
DROP TABLE IF EXISTS t1;
DROP FUNCTION IF EXISTS test.square;
--enable_warnings
CREATE TABLE t1 (i INT);
# Create function that returns a value from @var. In this case, the square function
CREATE FUNCTION test.square() RETURNS INTEGER DETERMINISTIC RETURN
(@var * @var);
# Set the @var to different values and insert them into a table
SET @var = 1;
INSERT INTO t1 VALUES (square());
SET @var = 2;
INSERT INTO t1 VALUES (square());
SET @var = 3;
INSERT INTO t1 VALUES (square());
SET @var = 4;
INSERT INTO t1 VALUES (square());
SET @var = 5;
INSERT INTO t1 VALUES (square());
--echo On master: Retrieve the values from the table
SELECT * FROM t1;
sync_slave_with_master;
--echo On slave: Retrieve the values from the table and verify they are the same as on master
SELECT * FROM t1;
connection master;
# Cleanup
DROP TABLE t1;
DROP FUNCTION test.square;
# This test uses stored functions that uses user-defined variables to return data
# based on the use of @vars inside a function body.
# This test was constructed for BUG#14914
--disable_warnings
DROP TABLE IF EXISTS t1;
DROP FUNCTION IF EXISTS f1;
DROP FUNCTION IF EXISTS f2;
--enable_warnings
CREATE TABLE t1(a int);
DELIMITER |;
# Create a function that simply returns the value of an @var.
# Create a function that uses an @var for flow control, creates and uses another
# @var and sets its value to a value based on another @var.
CREATE FUNCTION f1() returns int deterministic BEGIN
return @a;
END |
CREATE FUNCTION f2() returns int deterministic BEGIN
IF (@b > 0) then
SET @c = (@a + @b);
else
SET @c = (@a - 1);
END if;
return @c;
END |
DELIMITER ;|
sync_slave_with_master;
connection master;
# Set an @var to a value and insert data into a table using the first function.
# Set two more @vars to some values and insert data into a table using the second function.
SET @a=500;
INSERT INTO t1 values(f1());
SET @b = 125;
SET @c = 1;
INSERT INTO t1 values(f2());
--echo On master: Retrieve the values from the table
sync_slave_with_master;
connection master;
SELECT * from t1;
connection slave;
--echo On slave: Check the tables for correct data and it matches master
SELECT * from t1;
connection master;
# Cleanup
DROP TABLE t1;
DROP FUNCTION f1;
DROP FUNCTION f2;
# This test uses a function that changes a user-defined variable in its body. This test
# will ensure the @vars are replicated when needed and not interrupt the normal execution
# of the function on the slave. This also applies to procedures and triggers.
# This test was constructed for BUG#25167
--disable_warnings
DROP TABLE IF EXISTS t1;
DROP TABLE IF EXISTS t2;
--enable_warnings
CREATE TABLE t1 (i int);
CREATE TABLE t2 (k int);
DELIMITER |;
# Create a trigger that inserts data into another table, changes the @var then inserts
# another row with the modified value.
CREATE trigger t1_bi before INSERT on t1 for each row BEGIN
INSERT INTO t2 values (@a);
SET @a:=42;
INSERT INTO t2 values (@a);
END |
DELIMITER ;|
sync_slave_with_master;
connection master;
# Set the @var to a value then insert data into first table.
SET @a:=100;
INSERT INTO t1 values (5);
--echo On master: Check to see that data was inserted correctly in both tables
SELECT * from t1;
SELECT * from t2;
sync_slave_with_master;
--echo On slave: Check the tables for correct data and it matches master
SELECT * from t1;
SELECT * from t2;
connection master;
--echo End of 5.0 tests.
# Cleanup
DROP TABLE t1;
DROP TABLE t2;
# This test uses a stored function that uses user-defined variables to return data
# The test ensures the value of the user-defined variable is replicated correctly
# and in the correct order of assignment.
# This test was constructed for BUG#20141
--disable_warnings
DROP TABLE IF EXISTS t1;
DROP FUNCTION IF EXISTS f1;
DROP FUNCTION IF EXISTS f2;
--enable_warnings
CREATE TABLE t1 (i INT);
# Create two functions. One simply returns the user-defined variable. The other
# returns a value based on the user-defined variable.
CREATE FUNCTION f1() RETURNS INT RETURN @a; DELIMITER |; CREATE
FUNCTION f2() RETURNS INT BEGIN
INSERT INTO t1 VALUES (10 + @a);
RETURN 0;
END|
DELIMITER ;|
sync_slave_with_master;
connection master;
# Set the variable and execute the functions.
SET @a:=123;
SELECT f1(), f2();
--echo On master: Check to see that data was inserted correctly
INSERT INTO t1 VALUES(f1());
SELECT * FROM t1;
sync_slave_with_master;
--echo On slave: Check the table for correct data and it matches master
SELECT * FROM t1;
connection master;
# Cleanup
DROP FUNCTION f1;
DROP FUNCTION f2;
DROP TABLE t1;
sync_slave_with_master; sync_slave_with_master;
stop slave; stop slave;
...@@ -4220,7 +4220,14 @@ int get_var_with_binlog(THD *thd, enum_sql_command sql_command, ...@@ -4220,7 +4220,14 @@ int get_var_with_binlog(THD *thd, enum_sql_command sql_command,
user_var_entry *var_entry; user_var_entry *var_entry;
var_entry= get_variable(&thd->user_vars, name, 0); var_entry= get_variable(&thd->user_vars, name, 0);
if (!(opt_bin_log && is_update_query(sql_command))) /*
Any reference to user-defined variable which is done from stored
function or trigger affects their execution and execution of calling
statement. Hence we want to log all accesses to such variables and
not only those that happen from table-updating statement.
*/
if (!(opt_bin_log &&
(is_update_query(sql_command) || thd->in_sub_stmt)))
{ {
*out_entry= var_entry; *out_entry= var_entry;
return 0; return 0;
......
...@@ -3350,13 +3350,13 @@ bool MYSQL_BIN_LOG::flush_and_sync() ...@@ -3350,13 +3350,13 @@ bool MYSQL_BIN_LOG::flush_and_sync()
return err; return err;
} }
void MYSQL_BIN_LOG::start_union_events(THD *thd) void MYSQL_BIN_LOG::start_union_events(THD *thd, query_id_t query_id_param)
{ {
DBUG_ASSERT(!thd->binlog_evt_union.do_union); DBUG_ASSERT(!thd->binlog_evt_union.do_union);
thd->binlog_evt_union.do_union= TRUE; thd->binlog_evt_union.do_union= TRUE;
thd->binlog_evt_union.unioned_events= FALSE; thd->binlog_evt_union.unioned_events= FALSE;
thd->binlog_evt_union.unioned_events_trans= FALSE; thd->binlog_evt_union.unioned_events_trans= FALSE;
thd->binlog_evt_union.first_query_id= thd->query_id; thd->binlog_evt_union.first_query_id= query_id_param;
} }
void MYSQL_BIN_LOG::stop_union_events(THD *thd) void MYSQL_BIN_LOG::stop_union_events(THD *thd)
......
...@@ -341,7 +341,7 @@ class MYSQL_BIN_LOG: public TC_LOG, private MYSQL_LOG ...@@ -341,7 +341,7 @@ class MYSQL_BIN_LOG: public TC_LOG, private MYSQL_LOG
int write_cache(IO_CACHE *cache, bool lock_log, bool flush_and_sync); int write_cache(IO_CACHE *cache, bool lock_log, bool flush_and_sync);
void start_union_events(THD *thd); void start_union_events(THD *thd, query_id_t query_id_param);
void stop_union_events(THD *thd); void stop_union_events(THD *thd);
bool is_query_in_union(THD *thd, query_id_t query_id_param); bool is_query_in_union(THD *thd, query_id_t query_id_param);
......
...@@ -1485,8 +1485,24 @@ sp_head::execute_function(THD *thd, Item **argp, uint argcount, ...@@ -1485,8 +1485,24 @@ sp_head::execute_function(THD *thd, Item **argp, uint argcount,
if (need_binlog_call) if (need_binlog_call)
{ {
query_id_t q;
reset_dynamic(&thd->user_var_events); reset_dynamic(&thd->user_var_events);
mysql_bin_log.start_union_events(thd); /*
In case of artificially constructed events for function calls
we have separate union for each such event and hence can't use
query_id of real calling statement as the start of all these
unions (this will break logic of replication of user-defined
variables). So we use artifical value which is guaranteed to
be greater than all query_id's of all statements belonging
to previous events/unions.
Possible alternative to this is logging of all function invocations
as one select and not resetting THD::user_var_events before
each invocation.
*/
VOID(pthread_mutex_lock(&LOCK_thread_count));
q= ::query_id;
VOID(pthread_mutex_unlock(&LOCK_thread_count));
mysql_bin_log.start_union_events(thd, q + 1);
binlog_save_options= thd->options; binlog_save_options= thd->options;
thd->options&= ~OPTION_BIN_LOG; thd->options&= ~OPTION_BIN_LOG;
} }
......
...@@ -2158,6 +2158,11 @@ void THD::reset_sub_statement_state(Sub_statement_state *backup, ...@@ -2158,6 +2158,11 @@ void THD::reset_sub_statement_state(Sub_statement_state *backup,
{ {
options&= ~OPTION_BIN_LOG; options&= ~OPTION_BIN_LOG;
} }
if ((backup->options & OPTION_BIN_LOG) && is_update_query(lex->sql_command)&&
!current_stmt_binlog_row_based)
mysql_bin_log.start_union_events(this, this->query_id);
/* Disable result sets */ /* Disable result sets */
client_capabilities &= ~CLIENT_MULTI_RESULTS; client_capabilities &= ~CLIENT_MULTI_RESULTS;
in_sub_stmt|= new_state; in_sub_stmt|= new_state;
...@@ -2201,6 +2206,10 @@ void THD::restore_sub_statement_state(Sub_statement_state *backup) ...@@ -2201,6 +2206,10 @@ void THD::restore_sub_statement_state(Sub_statement_state *backup)
sent_row_count= backup->sent_row_count; sent_row_count= backup->sent_row_count;
client_capabilities= backup->client_capabilities; client_capabilities= backup->client_capabilities;
if ((options & OPTION_BIN_LOG) && is_update_query(lex->sql_command) &&
!current_stmt_binlog_row_based)
mysql_bin_log.stop_union_events(this);
/* /*
The following is added to the old values as we are interested in the The following is added to the old values as we are interested in the
total complexity of the query total complexity of the query
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment