2017-11-02 10:06:02 +01:00
|
|
|
/* Copyright (c) 2017, MariaDB
|
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation; version 2 of the License.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program; if not, write to the Free Software
|
|
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */
|
|
|
|
|
|
|
|
#include "mariadb.h"
|
2017-06-29 14:32:17 +02:00
|
|
|
#include "sql_list.h"
|
|
|
|
#include "sql_tvc.h"
|
|
|
|
#include "sql_class.h"
|
2017-08-29 02:32:39 +02:00
|
|
|
#include "opt_range.h"
|
|
|
|
#include "sql_select.h"
|
|
|
|
#include "sql_explain.h"
|
|
|
|
#include "sql_parse.h"
|
2017-11-02 10:06:02 +01:00
|
|
|
#include "sql_cte.h"
|
2017-09-04 22:29:58 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
@brief
|
|
|
|
Fix fields for TVC values
|
|
|
|
|
|
|
|
@param
|
|
|
|
@param thd The context of the statement
|
|
|
|
@param li The iterator on the list of lists
|
|
|
|
|
|
|
|
@details
|
|
|
|
Call fix_fields procedure for TVC values.
|
|
|
|
|
|
|
|
@retval
|
|
|
|
true if an error was reported
|
|
|
|
false otherwise
|
|
|
|
*/
|
|
|
|
|
|
|
|
bool fix_fields_for_tvc(THD *thd, List_iterator_fast<List_item> &li)
|
|
|
|
{
|
|
|
|
DBUG_ENTER("fix_fields_for_tvc");
|
|
|
|
List_item *lst;
|
|
|
|
li.rewind();
|
|
|
|
|
|
|
|
while ((lst= li++))
|
|
|
|
{
|
|
|
|
List_iterator_fast<Item> it(*lst);
|
|
|
|
Item *item;
|
|
|
|
|
|
|
|
while ((item= it++))
|
|
|
|
{
|
|
|
|
if (item->fix_fields(thd, 0))
|
|
|
|
DBUG_RETURN(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
DBUG_RETURN(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-06-29 14:32:17 +02:00
|
|
|
/**
|
2017-08-29 02:32:39 +02:00
|
|
|
@brief
|
|
|
|
Defines types of matrix columns elements where matrix rows are defined by
|
|
|
|
some lists of values.
|
|
|
|
|
|
|
|
@param
|
2017-11-02 05:42:26 +01:00
|
|
|
@param thd The context of the statement
|
|
|
|
@param li The iterator on the list of lists
|
|
|
|
@param holders The structure where types of matrix columns are stored
|
|
|
|
@param first_list_el_count Count of the list values. It should be the same
|
|
|
|
for each list of lists elements. It contains
|
|
|
|
number of elements of the first list from list of
|
|
|
|
lists.
|
2017-08-29 02:32:39 +02:00
|
|
|
|
|
|
|
@details
|
2017-11-02 05:42:26 +01:00
|
|
|
For each list list_a from list of lists the procedure gets its elements
|
|
|
|
types and aggregates them with the previous ones stored in holders. If
|
|
|
|
list_a is the first one in the list of lists its elements types are put in
|
|
|
|
holders. The errors can be reported when count of list_a elements is
|
|
|
|
different from the first_list_el_count. Also error can be reported whe
|
|
|
|
n aggregation can't be made.
|
2017-08-29 02:32:39 +02:00
|
|
|
|
|
|
|
@retval
|
|
|
|
true if an error was reported
|
|
|
|
false otherwise
|
2017-06-29 14:32:17 +02:00
|
|
|
*/
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
bool join_type_handlers_for_tvc(THD *thd, List_iterator_fast<List_item> &li,
|
2017-08-29 02:32:39 +02:00
|
|
|
Type_holder *holders, uint first_list_el_count)
|
2017-06-29 14:32:17 +02:00
|
|
|
{
|
2017-08-29 02:32:39 +02:00
|
|
|
DBUG_ENTER("join_type_handlers_for_tvc");
|
2017-06-29 14:32:17 +02:00
|
|
|
List_item *lst;
|
|
|
|
li.rewind();
|
|
|
|
bool first= true;
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
while ((lst= li++))
|
2017-06-29 14:32:17 +02:00
|
|
|
{
|
|
|
|
List_iterator_fast<Item> it(*lst);
|
|
|
|
Item *item;
|
|
|
|
|
2017-08-29 02:32:39 +02:00
|
|
|
if (first_list_el_count != lst->elements)
|
2017-06-29 14:32:17 +02:00
|
|
|
{
|
2017-08-29 16:58:32 +02:00
|
|
|
my_message(ER_WRONG_NUMBER_OF_VALUES_IN_TVC,
|
2017-09-04 22:29:58 +02:00
|
|
|
ER_THD(thd, ER_WRONG_NUMBER_OF_VALUES_IN_TVC),
|
2017-08-29 02:32:39 +02:00
|
|
|
MYF(0));
|
|
|
|
DBUG_RETURN(true);
|
2017-06-29 14:32:17 +02:00
|
|
|
}
|
|
|
|
for (uint pos= 0; (item=it++); pos++)
|
|
|
|
{
|
|
|
|
const Type_handler *item_type_handler= item->real_type_handler();
|
|
|
|
if (first)
|
|
|
|
holders[pos].set_handler(item_type_handler);
|
|
|
|
else if (holders[pos].aggregate_for_result(item_type_handler))
|
|
|
|
{
|
2017-08-29 02:32:39 +02:00
|
|
|
my_error(ER_ILLEGAL_PARAMETER_DATA_TYPES2_FOR_OPERATION, MYF(0),
|
|
|
|
holders[pos].type_handler()->name().ptr(),
|
|
|
|
item_type_handler->name().ptr(),
|
|
|
|
"TABLE VALUE CONSTRUCTOR");
|
|
|
|
DBUG_RETURN(true);
|
2017-06-29 14:32:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
first= false;
|
|
|
|
}
|
2017-08-29 02:32:39 +02:00
|
|
|
DBUG_RETURN(false);
|
2017-06-29 14:32:17 +02:00
|
|
|
}
|
|
|
|
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-06-29 14:32:17 +02:00
|
|
|
/**
|
2017-08-29 02:32:39 +02:00
|
|
|
@brief
|
2017-11-02 05:42:26 +01:00
|
|
|
Define attributes of matrix columns elements where matrix rows are defined
|
|
|
|
by some lists of values.
|
2017-08-29 02:32:39 +02:00
|
|
|
|
|
|
|
@param
|
2017-11-02 05:42:26 +01:00
|
|
|
@param thd The context of the statement
|
|
|
|
@param li The iterator on the list of lists
|
|
|
|
@param holders The structure where names of matrix columns are stored
|
|
|
|
@param count_of_lists Count of list of lists elements
|
|
|
|
@param first_list_el_count Count of the list values. It should be the same
|
|
|
|
for each list of lists elements. It contains
|
|
|
|
number of elements of the first list from list
|
|
|
|
of lists.
|
2017-08-29 02:32:39 +02:00
|
|
|
|
|
|
|
@details
|
2017-11-02 05:42:26 +01:00
|
|
|
For each list list_a from list of lists the procedure gets its elements
|
|
|
|
attributes and aggregates them with the previous ones stored in holders.
|
2017-08-29 02:32:39 +02:00
|
|
|
The errors can be reported when aggregation can't be made.
|
|
|
|
|
|
|
|
@retval
|
|
|
|
true if an error was reported
|
|
|
|
false otherwise
|
2017-06-29 14:32:17 +02:00
|
|
|
*/
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
bool get_type_attributes_for_tvc(THD *thd,
|
2017-06-29 14:32:17 +02:00
|
|
|
List_iterator_fast<List_item> &li,
|
2017-08-29 02:32:39 +02:00
|
|
|
Type_holder *holders, uint count_of_lists,
|
|
|
|
uint first_list_el_count)
|
2017-06-29 14:32:17 +02:00
|
|
|
{
|
2017-08-29 02:32:39 +02:00
|
|
|
DBUG_ENTER("get_type_attributes_for_tvc");
|
2017-06-29 14:32:17 +02:00
|
|
|
List_item *lst;
|
|
|
|
li.rewind();
|
|
|
|
|
|
|
|
for (uint pos= 0; pos < first_list_el_count; pos++)
|
|
|
|
{
|
2017-09-04 22:29:58 +02:00
|
|
|
if (holders[pos].alloc_arguments(thd, count_of_lists))
|
2017-08-29 02:32:39 +02:00
|
|
|
DBUG_RETURN(true);
|
2017-06-29 14:32:17 +02:00
|
|
|
}
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
while ((lst= li++))
|
2017-06-29 14:32:17 +02:00
|
|
|
{
|
2017-08-29 02:32:39 +02:00
|
|
|
List_iterator_fast<Item> it(*lst);
|
|
|
|
Item *item;
|
|
|
|
for (uint holder_pos= 0 ; (item= it++); holder_pos++)
|
|
|
|
{
|
|
|
|
DBUG_ASSERT(item->fixed);
|
|
|
|
holders[holder_pos].add_argument(item);
|
|
|
|
}
|
2017-06-29 14:32:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for (uint pos= 0; pos < first_list_el_count; pos++)
|
|
|
|
{
|
2017-09-04 22:29:58 +02:00
|
|
|
if (holders[pos].aggregate_attributes(thd))
|
2017-08-29 02:32:39 +02:00
|
|
|
DBUG_RETURN(true);
|
2017-06-29 14:32:17 +02:00
|
|
|
}
|
2017-08-29 02:32:39 +02:00
|
|
|
DBUG_RETURN(false);
|
2017-06-29 14:32:17 +02:00
|
|
|
}
|
|
|
|
|
2017-08-29 02:32:39 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
@brief
|
|
|
|
Prepare of TVC
|
|
|
|
|
|
|
|
@param
|
2017-09-04 22:29:58 +02:00
|
|
|
@param thd The context of the statement
|
2017-08-29 02:32:39 +02:00
|
|
|
@param sl The select where this TVC is defined
|
|
|
|
@param tmp_result Structure that contains the information
|
2017-09-04 22:29:58 +02:00
|
|
|
about where to send the result of the query
|
2017-08-29 02:32:39 +02:00
|
|
|
@param unit_arg The union where sl is defined
|
|
|
|
|
|
|
|
@details
|
|
|
|
Gets types and attributes of values of this TVC that will be used
|
|
|
|
for temporary table creation for this TVC. It creates Item_type_holders
|
|
|
|
for each element of the first list from list of lists (VALUES from tvc),
|
|
|
|
using its elements name, defined type and attribute.
|
|
|
|
|
|
|
|
@retval
|
|
|
|
true if an error was reported
|
|
|
|
false otherwise
|
|
|
|
*/
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
bool table_value_constr::prepare(THD *thd, SELECT_LEX *sl,
|
2017-08-29 02:32:39 +02:00
|
|
|
select_result *tmp_result,
|
|
|
|
st_select_lex_unit *unit_arg)
|
2017-06-29 14:32:17 +02:00
|
|
|
{
|
2017-08-29 02:32:39 +02:00
|
|
|
DBUG_ENTER("table_value_constr::prepare");
|
2017-10-28 20:54:18 +02:00
|
|
|
select_lex->in_tvc= true;
|
2017-06-29 14:32:17 +02:00
|
|
|
List_iterator_fast<List_item> li(lists_of_values);
|
|
|
|
|
|
|
|
List_item *first_elem= li++;
|
|
|
|
uint cnt= first_elem->elements;
|
|
|
|
Type_holder *holders;
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
if (fix_fields_for_tvc(thd, li))
|
|
|
|
DBUG_RETURN(true);
|
|
|
|
|
|
|
|
if (!(holders= new (thd->mem_root)
|
2017-06-29 14:32:17 +02:00
|
|
|
Type_holder[cnt]) ||
|
2017-09-04 22:29:58 +02:00
|
|
|
join_type_handlers_for_tvc(thd, li, holders,
|
2017-08-29 02:32:39 +02:00
|
|
|
cnt) ||
|
2017-09-04 22:29:58 +02:00
|
|
|
get_type_attributes_for_tvc(thd, li, holders,
|
2017-08-29 02:32:39 +02:00
|
|
|
lists_of_values.elements, cnt))
|
|
|
|
DBUG_RETURN(true);
|
2017-06-29 14:32:17 +02:00
|
|
|
|
|
|
|
List_iterator_fast<Item> it(*first_elem);
|
|
|
|
Item *item;
|
|
|
|
|
|
|
|
sl->item_list.empty();
|
|
|
|
for (uint pos= 0; (item= it++); pos++)
|
|
|
|
{
|
|
|
|
/* Error's in 'new' will be detected after loop */
|
2017-09-04 22:29:58 +02:00
|
|
|
Item_type_holder *new_holder= new (thd->mem_root)
|
|
|
|
Item_type_holder(thd,
|
2017-06-29 14:32:17 +02:00
|
|
|
&item->name,
|
|
|
|
holders[pos].type_handler(),
|
|
|
|
&holders[pos]/*Type_all_attributes*/,
|
|
|
|
holders[pos].get_maybe_null());
|
2017-09-04 22:29:58 +02:00
|
|
|
new_holder->fix_fields(thd, 0);
|
2017-06-29 14:32:17 +02:00
|
|
|
sl->item_list.push_back(new_holder);
|
|
|
|
}
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
if (thd->is_fatal_error)
|
2017-08-29 02:32:39 +02:00
|
|
|
DBUG_RETURN(true); // out of memory
|
2017-06-29 14:32:17 +02:00
|
|
|
|
|
|
|
result= tmp_result;
|
|
|
|
|
2017-08-29 02:32:39 +02:00
|
|
|
if (result && result->prepare(sl->item_list, unit_arg))
|
|
|
|
DBUG_RETURN(true);
|
|
|
|
|
2017-10-28 20:54:18 +02:00
|
|
|
select_lex->in_tvc= false;
|
2017-08-29 02:32:39 +02:00
|
|
|
DBUG_RETURN(false);
|
2017-06-29 14:32:17 +02:00
|
|
|
}
|
|
|
|
|
2017-08-29 02:32:39 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
Save Query Plan Footprint
|
|
|
|
*/
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
int table_value_constr::save_explain_data_intern(THD *thd,
|
2017-08-29 02:32:39 +02:00
|
|
|
Explain_query *output)
|
2017-06-29 14:32:17 +02:00
|
|
|
{
|
2017-08-29 02:32:39 +02:00
|
|
|
const char *message= "No tables used";
|
|
|
|
DBUG_ENTER("table_value_constr::save_explain_data_intern");
|
|
|
|
DBUG_PRINT("info", ("Select 0x%lx, type %s, message %s",
|
|
|
|
(ulong)select_lex, select_lex->type,
|
|
|
|
message));
|
|
|
|
DBUG_ASSERT(have_query_plan == QEP_AVAILABLE);
|
|
|
|
|
|
|
|
/* There should be no attempts to save query plans for merged selects */
|
|
|
|
DBUG_ASSERT(!select_lex->master_unit()->derived ||
|
|
|
|
select_lex->master_unit()->derived->is_materialized_derived() ||
|
|
|
|
select_lex->master_unit()->derived->is_with_table());
|
|
|
|
|
|
|
|
explain= new (output->mem_root) Explain_select(output->mem_root,
|
2017-09-04 22:29:58 +02:00
|
|
|
thd->lex->analyze_stmt);
|
2017-08-29 02:32:39 +02:00
|
|
|
select_lex->set_explain_type(true);
|
|
|
|
|
|
|
|
explain->select_id= select_lex->select_number;
|
|
|
|
explain->select_type= select_lex->type;
|
|
|
|
explain->linkage= select_lex->linkage;
|
|
|
|
explain->using_temporary= NULL;
|
|
|
|
explain->using_filesort= NULL;
|
|
|
|
/* Setting explain->message means that all other members are invalid */
|
|
|
|
explain->message= message;
|
|
|
|
|
|
|
|
if (select_lex->master_unit()->derived)
|
|
|
|
explain->connection_type= Explain_node::EXPLAIN_NODE_DERIVED;
|
|
|
|
|
|
|
|
output->add_node(explain);
|
|
|
|
|
|
|
|
if (select_lex->is_top_level_node())
|
|
|
|
output->query_plan_ready();
|
|
|
|
|
|
|
|
DBUG_RETURN(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
Optimization of TVC
|
|
|
|
*/
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
void table_value_constr::optimize(THD *thd)
|
2017-08-29 02:32:39 +02:00
|
|
|
{
|
2017-09-04 22:29:58 +02:00
|
|
|
create_explain_query_if_not_exists(thd->lex, thd->mem_root);
|
2017-08-29 02:32:39 +02:00
|
|
|
have_query_plan= QEP_AVAILABLE;
|
|
|
|
|
|
|
|
if (select_lex->select_number != UINT_MAX &&
|
|
|
|
select_lex->select_number != INT_MAX /* this is not a UNION's "fake select */ &&
|
|
|
|
have_query_plan != QEP_NOT_PRESENT_YET &&
|
2017-09-04 22:29:58 +02:00
|
|
|
thd->lex->explain && // for "SET" command in SPs.
|
|
|
|
(!thd->lex->explain->get_select(select_lex->select_number)))
|
2017-08-29 02:32:39 +02:00
|
|
|
{
|
2017-09-04 22:29:58 +02:00
|
|
|
save_explain_data_intern(thd, thd->lex->explain);
|
2017-08-29 02:32:39 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
Execute of TVC
|
|
|
|
*/
|
|
|
|
|
|
|
|
bool table_value_constr::exec(SELECT_LEX *sl)
|
|
|
|
{
|
|
|
|
DBUG_ENTER("table_value_constr::exec");
|
2017-06-29 14:32:17 +02:00
|
|
|
List_iterator_fast<List_item> li(lists_of_values);
|
|
|
|
List_item *elem;
|
|
|
|
|
2017-08-29 02:32:39 +02:00
|
|
|
if (select_options & SELECT_DESCRIBE)
|
|
|
|
DBUG_RETURN(false);
|
|
|
|
|
|
|
|
if (result->send_result_set_metadata(sl->item_list,
|
|
|
|
Protocol::SEND_NUM_ROWS |
|
|
|
|
Protocol::SEND_EOF))
|
|
|
|
{
|
|
|
|
DBUG_RETURN(true);
|
|
|
|
}
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
while ((elem= li++))
|
2017-06-29 14:32:17 +02:00
|
|
|
{
|
|
|
|
result->send_data(*elem);
|
|
|
|
}
|
2017-08-29 02:32:39 +02:00
|
|
|
|
|
|
|
if (result->send_eof())
|
|
|
|
DBUG_RETURN(true);
|
|
|
|
|
|
|
|
DBUG_RETURN(false);
|
|
|
|
}
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
|
2017-08-29 02:32:39 +02:00
|
|
|
/**
|
|
|
|
@brief
|
2017-09-04 22:29:58 +02:00
|
|
|
Print list
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
@param str The reference on the string representation of the list
|
|
|
|
@param list The list that needed to be print
|
2017-08-29 02:32:39 +02:00
|
|
|
@param query_type The mode of printing
|
|
|
|
|
|
|
|
@details
|
2017-09-04 22:29:58 +02:00
|
|
|
The method saves a string representation of list in the
|
|
|
|
string str.
|
2017-08-29 02:32:39 +02:00
|
|
|
*/
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
void print_list_item(String *str, List_item *list,
|
|
|
|
enum_query_type query_type)
|
2017-08-29 02:32:39 +02:00
|
|
|
{
|
2017-09-04 22:29:58 +02:00
|
|
|
bool is_first_elem= true;
|
|
|
|
List_iterator_fast<Item> it(*list);
|
|
|
|
Item *item;
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
str->append('(');
|
|
|
|
|
|
|
|
while ((item= it++))
|
2017-08-29 02:32:39 +02:00
|
|
|
{
|
2017-09-04 22:29:58 +02:00
|
|
|
if (is_first_elem)
|
|
|
|
is_first_elem= false;
|
2017-08-29 02:32:39 +02:00
|
|
|
else
|
|
|
|
str->append(',');
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
item->print(str, query_type);
|
2017-08-29 02:32:39 +02:00
|
|
|
}
|
2017-09-04 22:29:58 +02:00
|
|
|
|
|
|
|
str->append(')');
|
2017-08-29 02:32:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@brief
|
|
|
|
Print this TVC
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
@param thd The context of the statement
|
|
|
|
@param str The reference on the string representation of this TVC
|
2017-08-29 02:32:39 +02:00
|
|
|
@param query_type The mode of printing
|
|
|
|
|
|
|
|
@details
|
2017-09-04 22:29:58 +02:00
|
|
|
The method saves a string representation of this TVC in the
|
|
|
|
string str.
|
2017-08-29 02:32:39 +02:00
|
|
|
*/
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
void table_value_constr::print(THD *thd, String *str,
|
2017-08-29 02:32:39 +02:00
|
|
|
enum_query_type query_type)
|
|
|
|
{
|
2017-09-04 22:29:58 +02:00
|
|
|
DBUG_ASSERT(thd);
|
|
|
|
|
|
|
|
str->append(STRING_WITH_LEN("values "));
|
|
|
|
|
|
|
|
bool is_first_elem= true;
|
|
|
|
List_iterator_fast<List_item> li(lists_of_values);
|
|
|
|
List_item *list;
|
|
|
|
|
|
|
|
while ((list= li++))
|
|
|
|
{
|
|
|
|
if (is_first_elem)
|
|
|
|
is_first_elem= false;
|
|
|
|
else
|
|
|
|
str->append(',');
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
print_list_item(str, list, query_type);
|
|
|
|
}
|
2017-08-29 02:32:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@brief
|
2017-09-02 23:19:20 +02:00
|
|
|
Create list of lists for TVC from the list of this IN predicate
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
@param thd The context of the statement
|
|
|
|
@param values TVC list of values
|
2017-08-29 02:32:39 +02:00
|
|
|
|
|
|
|
@details
|
2017-09-02 23:19:20 +02:00
|
|
|
The method uses the list of values of this IN predicate to build
|
|
|
|
an equivalent list of values that can be used in TVC.
|
|
|
|
|
|
|
|
E.g.:
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
<value_list> = 5,2,7
|
|
|
|
<transformed_value_list> = (5),(2),(7)
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
<value_list> = (5,2),(7,1)
|
|
|
|
<transformed_value_list> = (5,2),(7,1)
|
2017-08-29 02:32:39 +02:00
|
|
|
|
|
|
|
@retval
|
2017-09-02 23:19:20 +02:00
|
|
|
false if the method succeeds
|
|
|
|
true otherwise
|
2017-08-29 02:32:39 +02:00
|
|
|
*/
|
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
bool Item_func_in::create_value_list_for_tvc(THD *thd,
|
|
|
|
List< List<Item> > *values)
|
2017-08-29 02:32:39 +02:00
|
|
|
{
|
2017-09-02 23:19:20 +02:00
|
|
|
bool is_list_of_rows= args[1]->type() == Item::ROW_ITEM;
|
2017-09-01 19:01:06 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
for (uint i=1; i < arg_count; i++)
|
|
|
|
{
|
|
|
|
List<Item> *tvc_value;
|
|
|
|
if (!(tvc_value= new (thd->mem_root) List<Item>()))
|
|
|
|
return true;
|
|
|
|
|
|
|
|
if (is_list_of_rows)
|
|
|
|
{
|
|
|
|
Item_row *row_list= (Item_row *)(args[i]);
|
|
|
|
|
|
|
|
for (uint j=0; j < row_list->cols(); j++)
|
|
|
|
{
|
|
|
|
if (tvc_value->push_back(row_list->element_index(j),
|
|
|
|
thd->mem_root))
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (tvc_value->push_back(args[i]))
|
|
|
|
return true;
|
|
|
|
|
|
|
|
if (values->push_back(tvc_value, thd->mem_root))
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
2017-08-29 02:32:39 +02:00
|
|
|
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
/**
|
|
|
|
@brief
|
|
|
|
Create name for the derived table defined by TVC
|
|
|
|
|
|
|
|
@param thd The context of the statement
|
|
|
|
@param parent_select The SELECT where derived table is used
|
|
|
|
@param alias The returned created name
|
|
|
|
|
|
|
|
@details
|
|
|
|
Create name for the derived table using current TVC number
|
|
|
|
for this parent_select stored in parent_select
|
|
|
|
|
|
|
|
@retval
|
|
|
|
true if creation fails
|
|
|
|
false otherwise
|
|
|
|
*/
|
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
static bool create_tvc_name(THD *thd, st_select_lex *parent_select,
|
|
|
|
LEX_CSTRING *alias)
|
|
|
|
{
|
2017-08-29 02:32:39 +02:00
|
|
|
char buff[6];
|
2017-09-01 19:01:06 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
alias->length= my_snprintf(buff, sizeof(buff),
|
|
|
|
"tvc_%u", parent_select->curr_tvc_name);
|
|
|
|
alias->str= thd->strmake(buff, alias->length);
|
|
|
|
if (!alias->str)
|
|
|
|
return true;
|
2017-09-01 19:01:06 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
return false;
|
|
|
|
}
|
2017-09-01 19:01:06 +02:00
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
|
2017-10-28 20:54:18 +02:00
|
|
|
bool Item_subselect::wrap_tvc_in_derived_table(THD *thd,
|
|
|
|
st_select_lex *tvc_sl)
|
|
|
|
{
|
|
|
|
LEX *lex= thd->lex;
|
|
|
|
/* SELECT_LEX object where the transformation is performed */
|
|
|
|
SELECT_LEX *parent_select= lex->current_select;
|
|
|
|
uint8 save_derived_tables= lex->derived_tables;
|
|
|
|
|
|
|
|
Query_arena backup;
|
|
|
|
Query_arena *arena= thd->activate_stmt_arena_if_needed(&backup);
|
|
|
|
|
|
|
|
/*
|
|
|
|
Create SELECT_LEX of the subquery SQ used in the result of transformation
|
|
|
|
*/
|
|
|
|
lex->current_select= tvc_sl;
|
|
|
|
if (mysql_new_select(lex, 0, NULL))
|
|
|
|
goto err;
|
|
|
|
mysql_init_select(lex);
|
|
|
|
/* Create item list as '*' for the subquery SQ */
|
|
|
|
Item *item;
|
|
|
|
SELECT_LEX *sq_select; // select for IN subquery;
|
|
|
|
sq_select= lex->current_select;
|
|
|
|
sq_select->linkage= tvc_sl->linkage;
|
|
|
|
sq_select->parsing_place= SELECT_LIST;
|
|
|
|
item= new (thd->mem_root) Item_field(thd, &sq_select->context,
|
|
|
|
NULL, NULL, &star_clex_str);
|
|
|
|
if (item == NULL || add_item_to_list(thd, item))
|
|
|
|
goto err;
|
|
|
|
(sq_select->with_wild)++;
|
|
|
|
|
|
|
|
/* Exclude SELECT with TVC */
|
|
|
|
tvc_sl->exclude();
|
|
|
|
/*
|
|
|
|
Create derived table DT that will wrap TVC in the result of transformation
|
|
|
|
*/
|
|
|
|
SELECT_LEX *tvc_select; // select for tvc
|
|
|
|
SELECT_LEX_UNIT *derived_unit; // unit for tvc_select
|
|
|
|
if (mysql_new_select(lex, 1, tvc_sl))
|
|
|
|
goto err;
|
|
|
|
tvc_select= lex->current_select;
|
|
|
|
derived_unit= tvc_select->master_unit();
|
|
|
|
tvc_select->linkage= DERIVED_TABLE_TYPE;
|
|
|
|
|
|
|
|
lex->current_select= sq_select;
|
|
|
|
|
|
|
|
/*
|
|
|
|
Create the name of the wrapping derived table and
|
|
|
|
add it to the FROM list of the subquery SQ
|
|
|
|
*/
|
|
|
|
Table_ident *ti;
|
|
|
|
LEX_CSTRING alias;
|
|
|
|
TABLE_LIST *derived_tab;
|
|
|
|
if (!(ti= new (thd->mem_root) Table_ident(derived_unit)) ||
|
|
|
|
create_tvc_name(thd, parent_select, &alias))
|
|
|
|
goto err;
|
|
|
|
if (!(derived_tab=
|
|
|
|
sq_select->add_table_to_list(thd,
|
|
|
|
ti, &alias, 0,
|
|
|
|
TL_READ, MDL_SHARED_READ)))
|
|
|
|
goto err;
|
|
|
|
sq_select->add_joined_table(derived_tab);
|
|
|
|
sq_select->add_where_field(derived_unit->first_select());
|
|
|
|
sq_select->context.table_list= sq_select->table_list.first;
|
|
|
|
sq_select->context.first_name_resolution_table= sq_select->table_list.first;
|
|
|
|
sq_select->table_list.first->derived_type= DTYPE_TABLE | DTYPE_MATERIALIZE;
|
|
|
|
lex->derived_tables|= DERIVED_SUBQUERY;
|
|
|
|
|
|
|
|
sq_select->where= 0;
|
|
|
|
sq_select->set_braces(false);
|
|
|
|
derived_unit->set_with_clause(0);
|
|
|
|
|
|
|
|
if (engine->engine_type() == subselect_engine::SINGLE_SELECT_ENGINE)
|
|
|
|
((subselect_single_select_engine *) engine)->change_select(sq_select);
|
|
|
|
|
|
|
|
if (arena)
|
|
|
|
thd->restore_active_arena(arena, &backup);
|
|
|
|
lex->current_select= sq_select;
|
|
|
|
return false;
|
|
|
|
|
|
|
|
err:
|
|
|
|
if (arena)
|
|
|
|
thd->restore_active_arena(arena, &backup);
|
|
|
|
lex->derived_tables= save_derived_tables;
|
|
|
|
lex->current_select= parent_select;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
/**
|
|
|
|
@brief
|
|
|
|
Transform IN predicate into IN subquery
|
2017-09-01 19:01:06 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
@param thd The context of the statement
|
|
|
|
@param arg Not used
|
2017-09-01 19:01:06 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
@details
|
|
|
|
The method transforms this IN predicate into in equivalent IN subquery:
|
2017-09-01 19:01:06 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
<left_expr> IN (<value_list>)
|
|
|
|
=>
|
|
|
|
<left_expr> IN (SELECT * FROM (VALUES <transformed_value_list>) AS tvc_#)
|
2017-09-01 19:01:06 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
E.g.:
|
|
|
|
|
|
|
|
<value_list> = 5,2,7
|
|
|
|
<transformed_value_list> = (5),(2),(7)
|
|
|
|
|
|
|
|
<value_list> = (5,2),(7,1)
|
|
|
|
<transformed_value_list> = (5,2),(7,1)
|
|
|
|
|
|
|
|
If the transformation succeeds the method returns the result IN subquery,
|
|
|
|
otherwise this IN predicate is returned.
|
|
|
|
|
|
|
|
@retval
|
|
|
|
pointer to the result of transformation if succeeded
|
|
|
|
pointer to this IN predicate otherwise
|
|
|
|
*/
|
|
|
|
|
|
|
|
Item *Item_func_in::in_predicate_to_in_subs_transformer(THD *thd,
|
|
|
|
uchar *arg)
|
|
|
|
{
|
|
|
|
if (!transform_into_subq)
|
|
|
|
return this;
|
2017-10-28 20:54:18 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
transform_into_subq= false;
|
|
|
|
|
|
|
|
List<List_item> values;
|
|
|
|
|
|
|
|
LEX *lex= thd->lex;
|
|
|
|
/* SELECT_LEX object where the transformation is performed */
|
|
|
|
SELECT_LEX *parent_select= lex->current_select;
|
|
|
|
uint8 save_derived_tables= lex->derived_tables;
|
2017-10-28 20:54:18 +02:00
|
|
|
|
|
|
|
for (uint i=1; i < arg_count; i++)
|
|
|
|
{
|
|
|
|
if (!args[i]->const_item())
|
|
|
|
return this;
|
|
|
|
}
|
2017-09-02 23:19:20 +02:00
|
|
|
|
|
|
|
Query_arena backup;
|
|
|
|
Query_arena *arena= thd->activate_stmt_arena_if_needed(&backup);
|
2017-08-29 02:32:39 +02:00
|
|
|
|
|
|
|
/*
|
2017-09-02 23:19:20 +02:00
|
|
|
Create SELECT_LEX of the subquery SQ used in the result of transformation
|
2017-08-29 02:32:39 +02:00
|
|
|
*/
|
|
|
|
if (mysql_new_select(lex, 1, NULL))
|
|
|
|
goto err;
|
|
|
|
mysql_init_select(lex);
|
2017-09-02 23:19:20 +02:00
|
|
|
/* Create item list as '*' for the subquery SQ */
|
|
|
|
Item *item;
|
|
|
|
SELECT_LEX *sq_select; // select for IN subquery;
|
|
|
|
sq_select= lex->current_select;
|
|
|
|
sq_select->parsing_place= SELECT_LIST;
|
|
|
|
item= new (thd->mem_root) Item_field(thd, &sq_select->context,
|
2017-09-01 19:01:06 +02:00
|
|
|
NULL, NULL, &star_clex_str);
|
2017-09-02 23:19:20 +02:00
|
|
|
if (item == NULL || add_item_to_list(thd, item))
|
2017-08-29 02:32:39 +02:00
|
|
|
goto err;
|
2017-09-02 23:19:20 +02:00
|
|
|
(sq_select->with_wild)++;
|
2017-08-29 02:32:39 +02:00
|
|
|
/*
|
2017-09-02 23:19:20 +02:00
|
|
|
Create derived table DT that will wrap TVC in the result of transformation
|
2017-08-29 02:32:39 +02:00
|
|
|
*/
|
2017-09-02 23:19:20 +02:00
|
|
|
SELECT_LEX *tvc_select; // select for tvc
|
|
|
|
SELECT_LEX_UNIT *derived_unit; // unit for tvc_select
|
2017-08-29 02:32:39 +02:00
|
|
|
if (mysql_new_select(lex, 1, NULL))
|
|
|
|
goto err;
|
|
|
|
mysql_init_select(lex);
|
2017-09-02 23:19:20 +02:00
|
|
|
tvc_select= lex->current_select;
|
|
|
|
derived_unit= tvc_select->master_unit();
|
|
|
|
tvc_select->linkage= DERIVED_TABLE_TYPE;
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
/* Create TVC used in the transformation */
|
|
|
|
if (create_value_list_for_tvc(thd, &values))
|
|
|
|
goto err;
|
|
|
|
if (!(tvc_select->tvc=
|
2017-09-01 19:01:06 +02:00
|
|
|
new (thd->mem_root)
|
|
|
|
table_value_constr(values,
|
2017-09-02 23:19:20 +02:00
|
|
|
tvc_select,
|
|
|
|
tvc_select->options)))
|
2017-08-29 02:32:39 +02:00
|
|
|
goto err;
|
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
lex->current_select= sq_select;
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
/*
|
|
|
|
Create the name of the wrapping derived table and
|
|
|
|
add it to the FROM list of the subquery SQ
|
|
|
|
*/
|
|
|
|
Table_ident *ti;
|
|
|
|
LEX_CSTRING alias;
|
|
|
|
TABLE_LIST *derived_tab;
|
|
|
|
if (!(ti= new (thd->mem_root) Table_ident(derived_unit)) ||
|
|
|
|
create_tvc_name(thd, parent_select, &alias))
|
2017-08-29 02:32:39 +02:00
|
|
|
goto err;
|
2017-09-02 23:19:20 +02:00
|
|
|
if (!(derived_tab=
|
|
|
|
sq_select->add_table_to_list(thd,
|
|
|
|
ti, &alias, 0,
|
|
|
|
TL_READ, MDL_SHARED_READ)))
|
2017-08-29 02:32:39 +02:00
|
|
|
goto err;
|
2017-09-02 23:19:20 +02:00
|
|
|
sq_select->add_joined_table(derived_tab);
|
|
|
|
sq_select->add_where_field(derived_unit->first_select());
|
|
|
|
sq_select->context.table_list= sq_select->table_list.first;
|
|
|
|
sq_select->context.first_name_resolution_table= sq_select->table_list.first;
|
|
|
|
sq_select->table_list.first->derived_type= DTYPE_TABLE | DTYPE_MATERIALIZE;
|
|
|
|
lex->derived_tables|= DERIVED_SUBQUERY;
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
sq_select->where= 0;
|
|
|
|
sq_select->set_braces(false);
|
|
|
|
derived_unit->set_with_clause(0);
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
/* Create IN subquery predicate */
|
|
|
|
sq_select->parsing_place= parent_select->parsing_place;
|
|
|
|
Item_in_subselect *in_subs;
|
|
|
|
if (!(in_subs=
|
|
|
|
new (thd->mem_root) Item_in_subselect(thd, args[0], sq_select)))
|
2017-09-01 19:01:06 +02:00
|
|
|
goto err;
|
|
|
|
in_subs->emb_on_expr_nest= emb_on_expr_nest;
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-01 19:01:06 +02:00
|
|
|
if (arena)
|
|
|
|
thd->restore_active_arena(arena, &backup);
|
2017-09-02 23:19:20 +02:00
|
|
|
thd->lex->current_select= parent_select;
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
if (in_subs->fix_fields(thd, (Item **)&in_subs))
|
|
|
|
goto err;
|
|
|
|
|
|
|
|
parent_select->curr_tvc_name++;
|
2017-09-01 19:01:06 +02:00
|
|
|
return in_subs;
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-01 19:01:06 +02:00
|
|
|
err:
|
|
|
|
if (arena)
|
|
|
|
thd->restore_active_arena(arena, &backup);
|
2017-09-02 23:19:20 +02:00
|
|
|
lex->derived_tables= save_derived_tables;
|
|
|
|
thd->lex->current_select= parent_select;
|
2017-09-04 22:29:58 +02:00
|
|
|
return NULL;
|
2017-08-29 02:32:39 +02:00
|
|
|
}
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
|
2017-08-29 02:32:39 +02:00
|
|
|
/**
|
|
|
|
@brief
|
2017-09-04 22:29:58 +02:00
|
|
|
Check if this IN-predicate can be transformed in IN-subquery
|
2017-08-29 02:32:39 +02:00
|
|
|
with TVC
|
|
|
|
|
|
|
|
@param thd The context of the statement
|
|
|
|
|
|
|
|
@details
|
2017-09-04 22:29:58 +02:00
|
|
|
Compare the number of elements in the list of
|
2017-08-29 02:32:39 +02:00
|
|
|
values in this IN-predicate with the
|
|
|
|
in_subquery_conversion_threshold special variable
|
|
|
|
|
|
|
|
@retval
|
|
|
|
true if transformation can be made
|
|
|
|
false otherwise
|
|
|
|
*/
|
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
bool Item_func_in::to_be_transformed_into_in_subq(THD *thd)
|
2017-08-29 02:32:39 +02:00
|
|
|
{
|
2017-09-04 22:29:58 +02:00
|
|
|
uint values_count= arg_count-1;
|
2017-08-29 02:32:39 +02:00
|
|
|
|
|
|
|
if (args[1]->type() == Item::ROW_ITEM)
|
2017-09-04 22:29:58 +02:00
|
|
|
values_count*= ((Item_row *)(args[1]))->cols();
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
if (values_count < thd->variables.in_subquery_conversion_threshold)
|
2017-08-29 02:32:39 +02:00
|
|
|
return false;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2017-09-04 22:29:58 +02:00
|
|
|
|
2017-08-29 02:32:39 +02:00
|
|
|
/**
|
|
|
|
@brief
|
2017-09-02 23:19:20 +02:00
|
|
|
Transform IN predicates into IN subqueries in WHERE and ON expressions
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
@param thd The context of the statement
|
2017-08-29 02:32:39 +02:00
|
|
|
|
|
|
|
@details
|
2017-09-02 23:19:20 +02:00
|
|
|
For each IN predicate from AND parts of the WHERE condition and/or
|
|
|
|
ON expressions of the SELECT for this join the method performs
|
|
|
|
the intransformation into an equivalent IN sunquery if it's needed.
|
|
|
|
|
|
|
|
@retval
|
|
|
|
false always
|
2017-08-29 02:32:39 +02:00
|
|
|
*/
|
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
bool JOIN::transform_in_predicates_into_in_subq(THD *thd)
|
2017-08-29 02:32:39 +02:00
|
|
|
{
|
2017-09-04 22:29:58 +02:00
|
|
|
DBUG_ENTER("JOIN::transform_in_predicates_into_in_subq");
|
2017-08-29 02:32:39 +02:00
|
|
|
if (!select_lex->in_funcs.elements)
|
2017-09-04 22:29:58 +02:00
|
|
|
DBUG_RETURN(false);
|
2017-08-29 02:32:39 +02:00
|
|
|
|
2017-09-02 23:19:20 +02:00
|
|
|
SELECT_LEX *save_current_select= thd->lex->current_select;
|
|
|
|
enum_parsing_place save_parsing_place= select_lex->parsing_place;
|
|
|
|
thd->lex->current_select= select_lex;
|
2017-08-29 02:32:39 +02:00
|
|
|
if (conds)
|
|
|
|
{
|
|
|
|
select_lex->parsing_place= IN_WHERE;
|
|
|
|
conds=
|
2017-09-02 23:19:20 +02:00
|
|
|
conds->transform(thd,
|
2017-08-29 02:32:39 +02:00
|
|
|
&Item::in_predicate_to_in_subs_transformer,
|
|
|
|
(uchar*) 0);
|
2017-09-04 22:29:58 +02:00
|
|
|
if (!conds)
|
|
|
|
DBUG_RETURN(true);
|
2017-09-02 23:19:20 +02:00
|
|
|
select_lex->prep_where= conds ? conds->copy_andor_structure(thd) : 0;
|
2017-08-29 02:32:39 +02:00
|
|
|
select_lex->where= conds;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (join_list)
|
|
|
|
{
|
|
|
|
TABLE_LIST *table;
|
|
|
|
List_iterator<TABLE_LIST> li(*join_list);
|
|
|
|
select_lex->parsing_place= IN_ON;
|
|
|
|
|
|
|
|
while ((table= li++))
|
|
|
|
{
|
|
|
|
if (table->on_expr)
|
|
|
|
{
|
|
|
|
table->on_expr=
|
2017-09-02 23:19:20 +02:00
|
|
|
table->on_expr->transform(thd,
|
2017-08-29 02:32:39 +02:00
|
|
|
&Item::in_predicate_to_in_subs_transformer,
|
|
|
|
(uchar*) 0);
|
2017-09-04 22:29:58 +02:00
|
|
|
if (!table->on_expr)
|
|
|
|
DBUG_RETURN(true);
|
2017-09-01 19:01:06 +02:00
|
|
|
table->prep_on_expr= table->on_expr ?
|
|
|
|
table->on_expr->copy_andor_structure(thd) : 0;
|
2017-08-29 02:32:39 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-09-02 23:19:20 +02:00
|
|
|
|
2017-09-01 19:01:06 +02:00
|
|
|
select_lex->in_funcs.empty();
|
2017-09-02 23:19:20 +02:00
|
|
|
select_lex->parsing_place= save_parsing_place;
|
|
|
|
thd->lex->current_select= save_current_select;
|
2017-09-04 22:29:58 +02:00
|
|
|
DBUG_RETURN(false);
|
2017-11-02 05:42:26 +01:00
|
|
|
}
|