From 2f11e5d1d310f333e4b0e870015afd588981b8ba Mon Sep 17 00:00:00 2001 From: "guilhem@gbichot2" <> Date: Thu, 2 Oct 2003 10:31:37 +0200 Subject: [PATCH 1/2] fix for BUG#1331: "Unexistent user variable is not replicated". When an update query is to be written to the binlog, and it reads unset user variables (example: INSERT INTO t VALUEs(@a) where @a does not exist), we create the variable like if it had been set with SET before, and we loop (i.e. we redo the steps of logging, now that the variable exists). --- mysql-test/r/rpl_user_variables.result | 50 +++++++-- mysql-test/t/rpl_user_variables.test | 5 +- sql/item_func.cc | 135 ++++++++++++++++++------- 3 files changed, 143 insertions(+), 47 deletions(-) diff --git a/mysql-test/r/rpl_user_variables.result b/mysql-test/r/rpl_user_variables.result index b715b750b68..71147772ac4 100644 --- a/mysql-test/r/rpl_user_variables.result +++ b/mysql-test/r/rpl_user_variables.result @@ -21,6 +21,7 @@ set @q:='abc'; insert t1 values (@q), (@q:=concat(@q, 'n1')), (@q:=concat(@q, 'n2')); set @a:=5; insert into t1 values (@a),(@a); +insert into t1 values (@a),(@a),(@a*5); select * from t1; n 12345678901234 @@ -45,6 +46,36 @@ abcn1 abcn1n2 5 5 +NULL +NULL +NULL +select * from t1; +n +12345678901234 +-12345678901234 +0 +-1 +12.5 +-12.5 +This is a test + +abc'def +abc\def +abc'def +NULL +NULL +0 +1 +2 +5 +abc +abcn1 +abcn1n2 +5 +5 +NULL +NULL +NULL show binlog events from 141; Log_name Pos Event_type Server_id Orig_log_pos Info slave-bin.000001 141 User var 2 141 @i1=12345678901234 @@ -63,13 +94,16 @@ slave-bin.000001 719 User var 2 719 @s5='abc'def' slave-bin.000001 761 Query 1 761 use `test`; insert into t1 values (@s1), (@s2), (@s3), (@s4), (@s5) slave-bin.000001 851 User var 2 851 @n1=NULL slave-bin.000001 877 Query 1 877 use `test`; insert into t1 values (@n1) -slave-bin.000001 939 Query 1 939 use `test`; insert into t1 values (@n2) -slave-bin.000001 1001 Query 1 1001 use `test`; insert into t1 values (@a:=0), (@a:=@a+1), (@a:=@a+1) -slave-bin.000001 1089 User var 2 1089 @a=2 -slave-bin.000001 1131 Query 1 1131 use `test`; insert into t1 values (@a+(@b:=@a+1)) -slave-bin.000001 1203 User var 2 1203 @q='abc' -slave-bin.000001 1240 Query 1 1240 use `test`; insert t1 values (@q), (@q:=concat(@q, 'n1')), (@q:=concat(@q, 'n2')) -slave-bin.000001 1344 User var 2 1344 @a=5 -slave-bin.000001 1386 Query 1 1386 use `test`; insert into t1 values (@a),(@a) +slave-bin.000001 939 User var 2 939 @n2=NULL +slave-bin.000001 965 Query 1 965 use `test`; insert into t1 values (@n2) +slave-bin.000001 1027 Query 1 1027 use `test`; insert into t1 values (@a:=0), (@a:=@a+1), (@a:=@a+1) +slave-bin.000001 1115 User var 2 1115 @a=2 +slave-bin.000001 1157 Query 1 1157 use `test`; insert into t1 values (@a+(@b:=@a+1)) +slave-bin.000001 1229 User var 2 1229 @q='abc' +slave-bin.000001 1266 Query 1 1266 use `test`; insert t1 values (@q), (@q:=concat(@q, 'n1')), (@q:=concat(@q, 'n2')) +slave-bin.000001 1370 User var 2 1370 @a=5 +slave-bin.000001 1412 Query 1 1412 use `test`; insert into t1 values (@a),(@a) +slave-bin.000001 1478 User var 2 1478 @a=NULL +slave-bin.000001 1503 Query 1 1503 use `test`; insert into t1 values (@a),(@a),(@a*5) drop table t1; stop slave; diff --git a/mysql-test/t/rpl_user_variables.test b/mysql-test/t/rpl_user_variables.test index 7eeccaf64f2..35fbec72ac8 100644 --- a/mysql-test/t/rpl_user_variables.test +++ b/mysql-test/t/rpl_user_variables.test @@ -29,13 +29,16 @@ insert into t1 values (@i1), (@i2), (@i3), (@i4); insert into t1 values (@r1), (@r2); insert into t1 values (@s1), (@s2), (@s3), (@s4), (@s5); insert into t1 values (@n1); -insert into t1 values (@n2); +insert into t1 values (@n2); # not explicitely set before insert into t1 values (@a:=0), (@a:=@a+1), (@a:=@a+1); insert into t1 values (@a+(@b:=@a+1)); set @q:='abc'; insert t1 values (@q), (@q:=concat(@q, 'n1')), (@q:=concat(@q, 'n2')); set @a:=5; insert into t1 values (@a),(@a); +connection master1; # see if variable is reset in binlog when thread changes +insert into t1 values (@a),(@a),(@a*5); +select * from t1; save_master_pos; connection slave; sync_with_master; diff --git a/sql/item_func.cc b/sql/item_func.cc index b7979e7909c..8073ee60572 100644 --- a/sql/item_func.cc +++ b/sql/item_func.cc @@ -2071,6 +2071,16 @@ static user_var_entry *get_variable(HASH *hash, LEX_STRING &name, entry->value=0; entry->length=0; entry->update_query_id=0; + /* + If we are here, we were called from a SET or a query which sets a + variable. Imagine it is this: + INSERT INTO t SELECT @a:=10, @a:=@a+1. + Then when we have a Item_func_get_user_var (because of the @a+1) so we + think we have to write the value of @a to the binlog. But before that, + we have a Item_func_set_user_var to create @a (@a:=10), in this we mark + the variable as "already logged" (line below) so that it won't be logged + by Item_func_get_user_var (because that's not necessary). + */ entry->used_query_id=current_thd->query_id; entry->type=STRING_RESULT; memcpy(entry->name.str, name.str, name.length+1); @@ -2083,7 +2093,10 @@ static user_var_entry *get_variable(HASH *hash, LEX_STRING &name, return entry; } - +/* + When a user variable is updated (in a SET command or a query like SELECT @a:= + ). +*/ bool Item_func_set_user_var::fix_fields(THD *thd, TABLE_LIST *tables, Item **ref) @@ -2093,6 +2106,11 @@ bool Item_func_set_user_var::fix_fields(THD *thd, TABLE_LIST *tables, !(entry= get_variable(&thd->user_vars, name, 1))) return 1; entry->type= cached_result_type; + /* + Remember the last query which updated it, this way a query can later know + if this variable is a constant item in the query (it is if update_query_id + is different from query_id). + */ entry->update_query_id=thd->query_id; return 0; } @@ -2315,53 +2333,94 @@ longlong Item_func_get_user_var::val_int() } +/* + When a user variable is invoked from an update query (INSERT, UPDATE etc), + stores this variable and its value in thd->user_var_events, so that it can be + written to the binlog (will be written just before the query is written, see + log.cc). +*/ + void Item_func_get_user_var::fix_length_and_dec() { - BINLOG_USER_VAR_EVENT *user_var_event; THD *thd=current_thd; + + if (!(opt_bin_log && is_update_query(thd->lex.sql_command))) + return; + + BINLOG_USER_VAR_EVENT *user_var_event; maybe_null=1; decimals=NOT_FIXED_DEC; max_length=MAX_BLOB_WIDTH; - if ((var_entry= get_variable(&thd->user_vars, name, 0))) + /* + If the variable does not exist, it's NULL, but we want to create it so + that it gets into the binlog (if it didn't, the slave could be + influenced by a variable of the same name previously set by another + thread). + */ + + if (!(var_entry= get_variable(&thd->user_vars, name, 0))) { - if (opt_bin_log && is_update_query(thd->lex.sql_command) && - var_entry->used_query_id != thd->query_id) - { - uint size; - /* - First we need to store value of var_entry, when the next situation - appers: - > set @a:=1; - > insert into t1 values (@a), (@a:=@a+1), (@a:=@a+1); - We have to write to binlog value @a= 1; - */ - size= ALIGN_SIZE(sizeof(BINLOG_USER_VAR_EVENT)) + var_entry->length; - if (!(user_var_event= (BINLOG_USER_VAR_EVENT *) thd->alloc(size))) - goto err; + /* + We create it like if it had been explicitely set with SET before. + The 'new' mimicks what sql_yacc.yy does when 'SET @a=10;'. + sql_set_variables() is what is called from 'case SQLCOM_SET_OPTION' + in dispatch_command()). Instead of building a one-element list to pass to + sql_set_variables(), we could instead manually call check() and update(); + this would save memory and time; but calling sql_set_variables() makes one + unique place to maintain (sql_set_variables()). + */ - user_var_event->value= (char*) user_var_event + - ALIGN_SIZE(sizeof(BINLOG_USER_VAR_EVENT)); - user_var_event->user_var_event= var_entry; - user_var_event->type= var_entry->type; - user_var_event->charset_number= var_entry->collation.collation->number; - if (!var_entry->value) - { - /* NULL value*/ - user_var_event->length= 0; - user_var_event->value= 0; - } - else - { - user_var_event->length= var_entry->length; - memcpy(user_var_event->value, var_entry->value, - var_entry->length); - } - var_entry->used_query_id= thd->query_id; - if (insert_dynamic(&thd->user_var_events, (gptr) &user_var_event)) - goto err; - } + List tmp_var_list; + tmp_var_list.push_back(new set_var_user(new Item_func_set_user_var(name, + new Item_null()))); + if (sql_set_variables(thd, &tmp_var_list)) /* this will create the variable */ + goto err; + if (!(var_entry= get_variable(&thd->user_vars, name, 0))) + goto err; } + /* + If this variable was already stored in user_var_events by this query + (because it's used in more than one place in the query), don't store + it. + */ + else if (var_entry->used_query_id == thd->query_id) + return; + + uint size; + /* + First we need to store value of var_entry, when the next situation + appers: + > set @a:=1; + > insert into t1 values (@a), (@a:=@a+1), (@a:=@a+1); + We have to write to binlog value @a= 1; + */ + size= ALIGN_SIZE(sizeof(BINLOG_USER_VAR_EVENT)) + var_entry->length; + if (!(user_var_event= (BINLOG_USER_VAR_EVENT *) thd->alloc(size))) + goto err; + + user_var_event->value= (char*) user_var_event + + ALIGN_SIZE(sizeof(BINLOG_USER_VAR_EVENT)); + user_var_event->user_var_event= var_entry; + user_var_event->type= var_entry->type; + user_var_event->charset_number= var_entry->collation.collation->number; + if (!var_entry->value) + { + /* NULL value*/ + user_var_event->length= 0; + user_var_event->value= 0; + } + else + { + user_var_event->length= var_entry->length; + memcpy(user_var_event->value, var_entry->value, + var_entry->length); + } + /* Mark that this variable has been used by this query */ + var_entry->used_query_id= thd->query_id; + if (insert_dynamic(&thd->user_var_events, (gptr) &user_var_event)) + goto err; + return; err: From d099c0ed40e0f5575407a904797821e09f38a24d Mon Sep 17 00:00:00 2001 From: "guilhem@gbichot2" <> Date: Thu, 2 Oct 2003 16:19:33 +0200 Subject: [PATCH 2/2] Had mangled the order of if()s in a previous changeset (1.1596) (not pushed), correcting it now. Thanks Dmitri for spotting this. --- mysql-test/r/user_var.result | 4 ++++ mysql-test/t/user_var.test | 3 +++ sql/item_func.cc | 22 ++++++++++------------ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/mysql-test/r/user_var.result b/mysql-test/r/user_var.result index 23253f60de9..b7c64551dc0 100644 --- a/mysql-test/r/user_var.result +++ b/mysql-test/r/user_var.result @@ -5,6 +5,10 @@ set @a := connection_id() + 3; select @a - connection_id(); @a - connection_id() 3 +set @b := 1; +select @b; +@b +1 CREATE TABLE t1 ( i int not null, v int not null,index (i)); insert into t1 values (1,1),(1,3),(2,1); create table t2 (i int not null, unique (i)); diff --git a/mysql-test/t/user_var.test b/mysql-test/t/user_var.test index 514eace25a3..947c944c79e 100644 --- a/mysql-test/t/user_var.test +++ b/mysql-test/t/user_var.test @@ -8,6 +8,9 @@ set @a := foo; set @a := connection_id() + 3; select @a - connection_id(); +set @b := 1; +select @b; + # Check using and setting variables with SELECT DISTINCT CREATE TABLE t1 ( i int not null, v int not null,index (i)); diff --git a/sql/item_func.cc b/sql/item_func.cc index 8073ee60572..0f9ee512be1 100644 --- a/sql/item_func.cc +++ b/sql/item_func.cc @@ -2343,25 +2343,23 @@ longlong Item_func_get_user_var::val_int() void Item_func_get_user_var::fix_length_and_dec() { THD *thd=current_thd; - - if (!(opt_bin_log && is_update_query(thd->lex.sql_command))) - return; - BINLOG_USER_VAR_EVENT *user_var_event; maybe_null=1; decimals=NOT_FIXED_DEC; max_length=MAX_BLOB_WIDTH; - /* - If the variable does not exist, it's NULL, but we want to create it so - that it gets into the binlog (if it didn't, the slave could be - influenced by a variable of the same name previously set by another - thread). - */ - - if (!(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(thd->lex.sql_command))) + return; + + if (!var_entry) { /* + If the variable does not exist, it's NULL, but we want to create it so + that it gets into the binlog (if it didn't, the slave could be + influenced by a variable of the same name previously set by another + thread). We create it like if it had been explicitely set with SET before. The 'new' mimicks what sql_yacc.yy does when 'SET @a=10;'. sql_set_variables() is what is called from 'case SQLCOM_SET_OPTION'