From e9cb71fc3a2f4e83720f1bab4f470e10ca878eb6 Mon Sep 17 00:00:00 2001 From: Gleb Shchepa Date: Thu, 18 Sep 2008 13:38:44 +0500 Subject: [PATCH] Bug#26020: User-Defined Variables are not consistent with columns data types The "SELECT @lastId, @lastId := Id FROM t" query returns different result sets depending on the type of the Id column (INT or BIGINT). Note: this fix doesn't cover the case when a select query references an user variable and stored function that updates a value of that variable, in this case a result is indeterminate. The server uses incorrect assumption about a constantness of an user variable value as a select list item: The server caches a last query number where that variable was changed and compares this number with a current query number. If these numbers are different, the server guesses, that the variable is not updating in the current query, so a respective select list item is a constant. However, in some common cases the server updates cached query number too late. The server has been modified to memorize user variable assignments during the parse phase to take them into account on the next (query preparation) phase independently of the order of user variable references/assignments in a select item list. mysql-test/r/user_var.result: Added test case for bug #26020. mysql-test/t/user_var.test: Added test case for bug #26020. sql/item_func.cc: An update of entry and update_query_id variables has been moved from Item_func_set_user_var::fix_fields() to a separate method, Item_func_set_user_var::set_entry(). sql/item_func.h: 1. The Item_func_set_user_var::set_entry() method has been added to update Item_func_set_user_var::entry. 2. The Item_func_set_user_var::entry_thd field has beend added to update Item_func_set_user_var::entry only when needed. sql/sql_base.cc: Fix: setup_fiedls() calls Item_func_set_user_var::set_entry() for all items from the thd->lex->set_var_list before the first call of ::fix_fields(). sql/sql_lex.cc: The lex_start function has been modified to reset the st_lex::set_var_list list. sql/sql_lex.h: New st_lex::set_var_list field has been added to memorize all user variable assignments in the current select query. sql/sql_yacc.yy: The variable_aux rule has been modified to memorize in-query user variable assignments in the st_lex::set_var_list list. --- mysql-test/r/user_var.result | 33 +++++++++++++++++++++++++++++++-- mysql-test/t/user_var.test | 22 ++++++++++++++++++++++ sql/item_func.cc | 27 +++++++++++++++++++-------- sql/item_func.h | 15 ++++++++++++++- sql/sql_base.cc | 16 ++++++++++++++++ sql/sql_lex.cc | 1 + sql/sql_lex.h | 1 + sql/sql_yacc.yy | 4 +++- 8 files changed, 107 insertions(+), 12 deletions(-) diff --git a/mysql-test/r/user_var.result b/mysql-test/r/user_var.result index 2cd132ce03c..2d91835d723 100644 --- a/mysql-test/r/user_var.result +++ b/mysql-test/r/user_var.result @@ -121,8 +121,8 @@ select @a:=0; select @a+0, @a:=@a+0+count(*), count(*), @a+0 from t1 group by i; @a+0 @a:=@a+0+count(*) count(*) @a+0 0 1 1 0 -1 3 2 0 -3 6 3 0 +0 2 2 0 +0 3 3 0 set @a=0; select @a,@a:="hello",@a,@a:=3,@a,@a:="hello again" from t1 group by i; @a @a:="hello" @a @a:=3 @a @a:="hello again" @@ -370,4 +370,33 @@ select @rownum := @rownum + 1 as row, @prev_score := a as score from t1 order by score desc; drop table t1; +create table t1(b bigint); +insert into t1 (b) values (10), (30), (10); +set @var := 0; +select if(b=@var, 999, b) , @var := b from t1 order by b; +if(b=@var, 999, b) @var := b +10 10 +999 10 +30 30 +drop table t1; +create temporary table t1 (id int); +insert into t1 values (2), (3), (3), (4); +set @lastid=-1; +select @lastid != id, @lastid, @lastid := id from t1; +@lastid != id @lastid @lastid := id +1 -1 2 +1 2 3 +0 3 3 +1 3 4 +drop table t1; +create temporary table t1 (id bigint); +insert into t1 values (2), (3), (3), (4); +set @lastid=-1; +select @lastid != id, @lastid, @lastid := id from t1; +@lastid != id @lastid @lastid := id +1 -1 2 +1 2 3 +0 3 3 +1 3 4 +drop table t1; End of 5.1 tests diff --git a/mysql-test/t/user_var.test b/mysql-test/t/user_var.test index f2699ab03d3..5d916e410e3 100644 --- a/mysql-test/t/user_var.test +++ b/mysql-test/t/user_var.test @@ -263,4 +263,26 @@ from t1 order by score desc; --enable_result_log drop table t1; +# +# Bug#26020: User-Defined Variables are not consistent with columns data types +# + +create table t1(b bigint); +insert into t1 (b) values (10), (30), (10); +set @var := 0; +select if(b=@var, 999, b) , @var := b from t1 order by b; +drop table t1; + +create temporary table t1 (id int); +insert into t1 values (2), (3), (3), (4); +set @lastid=-1; +select @lastid != id, @lastid, @lastid := id from t1; +drop table t1; + +create temporary table t1 (id bigint); +insert into t1 values (2), (3), (3), (4); +set @lastid=-1; +select @lastid != id, @lastid, @lastid := id from t1; +drop table t1; + --echo End of 5.1 tests diff --git a/sql/item_func.cc b/sql/item_func.cc index 8bb6bb30117..c1caf4039ed 100644 --- a/sql/item_func.cc +++ b/sql/item_func.cc @@ -3805,6 +3805,24 @@ static user_var_entry *get_variable(HASH *hash, LEX_STRING &name, return entry; } + +bool Item_func_set_user_var::set_entry(THD *thd, bool create_if_not_exists) +{ + if (thd == entry_thd && entry) + return FALSE; + entry_thd= thd; + if (!(entry= get_variable(&thd->user_vars, name, create_if_not_exists))) + return TRUE; + /* + 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 FALSE; +} + + /* When a user variable is updated (in a SET command or a query like SELECT @a:= ). @@ -3814,15 +3832,8 @@ bool Item_func_set_user_var::fix_fields(THD *thd, Item **ref) { DBUG_ASSERT(fixed == 0); /* fix_fields will call Item_func_set_user_var::fix_length_and_dec */ - if (Item_func::fix_fields(thd, ref) || - !(entry= get_variable(&thd->user_vars, name, 1))) + if (Item_func::fix_fields(thd, ref) || set_entry(thd, TRUE)) return TRUE; - /* - 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; /* As it is wrong and confusing to associate any character set with NULL, @a should be latin2 diff --git a/sql/item_func.h b/sql/item_func.h index 02631d7643d..d84abdb6e56 100644 --- a/sql/item_func.h +++ b/sql/item_func.h @@ -1294,6 +1294,17 @@ class Item_func_set_user_var :public Item_func { enum Item_result cached_result_type; user_var_entry *entry; + /* + The entry_thd variable is used: + 1) to skip unnecessary updates of the entry field (see above); + 2) to reset the entry field that was initialized in the other thread + (for example, an item tree of a trigger that updates user variables + may be shared between several connections, and the entry_thd field + prevents updates of one connection user variables from a concurrent + connection calling the same trigger that initially updated some + user variable it the first connection context). + */ + THD *entry_thd; char buffer[MAX_FIELD_WIDTH]; String value; my_decimal decimal_buff; @@ -1309,7 +1320,8 @@ class Item_func_set_user_var :public Item_func public: LEX_STRING name; // keep it public Item_func_set_user_var(LEX_STRING a,Item *b) - :Item_func(b), cached_result_type(INT_RESULT), name(a) + :Item_func(b), cached_result_type(INT_RESULT), + entry(NULL), entry_thd(NULL), name(a) {} enum Functype functype() const { return SUSERVAR_FUNC; } double val_real(); @@ -1340,6 +1352,7 @@ public: } void save_org_in_field(Field *field) { (void)save_in_field(field, 1, 0); } bool register_field_in_read_map(uchar *arg); + bool set_entry(THD *thd, bool create_if_not_exists); }; diff --git a/sql/sql_base.cc b/sql/sql_base.cc index 39030462f0b..6c5e58e2ad5 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -7296,6 +7296,22 @@ bool setup_fields(THD *thd, Item **ref_pointer_array, if (ref_pointer_array) bzero(ref_pointer_array, sizeof(Item *) * fields.elements); + /* + We call set_entry() there (before fix_fields() of the whole list of field + items) because: + 1) the list of field items has same order as in the query, and the + Item_func_get_user_var item may go before the Item_func_set_user_var: + SELECT @a, @a := 10 FROM t; + 2) The entry->update_query_id value controls constantness of + Item_func_get_user_var items, so in presence of Item_func_set_user_var + items we have to refresh their entries before fixing of + Item_func_get_user_var items. + */ + List_iterator li(thd->lex->set_var_list); + Item_func_set_user_var *var; + while ((var= li++)) + var->set_entry(thd, FALSE); + Item **ref= ref_pointer_array; thd->lex->current_select->cur_pos_in_select_list= 0; while ((item= it++)) diff --git a/sql/sql_lex.cc b/sql/sql_lex.cc index 1822176f00a..18c61910e89 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -293,6 +293,7 @@ void lex_start(THD *thd) lex->select_lex.init_query(); lex->value_list.empty(); lex->update_list.empty(); + lex->set_var_list.empty(); lex->param_list.empty(); lex->view_list.empty(); lex->prepared_stmt_params.empty(); diff --git a/sql/sql_lex.h b/sql/sql_lex.h index bb3dc00fc8d..7674b5c7ac2 100644 --- a/sql/sql_lex.h +++ b/sql/sql_lex.h @@ -1549,6 +1549,7 @@ typedef struct st_lex : public Query_tables_list List *insert_list,field_list,value_list,update_list; List many_values; List var_list; + List set_var_list; // in-query assignment list List param_list; List view_list; // view list (list of field names in view) /* diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 7ed35b65789..4a8796f20e5 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -8064,11 +8064,13 @@ variable: variable_aux: ident_or_text SET_VAR expr { - $$= new (YYTHD->mem_root) Item_func_set_user_var($1, $3); + Item_func_set_user_var *item; + $$= item= new (YYTHD->mem_root) Item_func_set_user_var($1, $3); if ($$ == NULL) MYSQL_YYABORT; LEX *lex= Lex; lex->uncacheable(UNCACHEABLE_RAND); + lex->set_var_list.push_back(item); } | ident_or_text {