Commit abf4a910 authored by Sergey Petrunya's avatar Sergey Petrunya

[SHOW] EXPLAIN UPDATE/DELETE

- Make QPF structures store data members, not strings. 
  This is not fully possible, because table names (and hence 
  key names, etc) can be deleted, and we have to store strings.
parent 69c386d9
/*
TODO MP AB copyright
*/
Copyright (c) 2013 Monty Program Ab
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */
#ifdef USE_PRAGMA_IMPLEMENTATION
#pragma implementation // gcc: Class implementation
......@@ -309,6 +321,11 @@ int QPF_table_access::print_explain(select_result_sink *output, uint8 explain_fl
uint select_id, const char *select_type,
bool using_temporary, bool using_filesort)
{
const CHARSET_INFO *cs= system_charset_info;
const char *hash_key_prefix= "#hash#";
bool is_hj= (type == JT_HASH || type == JT_HASH_NEXT ||
type == JT_HASH_RANGE || type == JT_HASH_INDEX_MERGE);
List<Item> item_list;
Item *item_null= new Item_null();
//const CHARSET_INFO *cs= system_charset_info;
......@@ -349,14 +366,62 @@ int QPF_table_access::print_explain(select_result_sink *output, uint8 explain_fl
item_list.push_back(item_null);
/* `key` */
if (key_set)
push_string(&item_list, &key);
StringBuffer<64> key_str;
if (key.key_name)
{
if (is_hj)
key_str.append(hash_key_prefix, strlen(hash_key_prefix), cs);
key_str.append(key.key_name);
if (is_hj && type != JT_HASH)
key_str.append(':');
}
if (quick_info)
{
StringBuffer<64> buf2;
quick_info->print_key(&buf2);
key_str.append(buf2);
}
if (type == JT_HASH_NEXT)
key_str.append(hash_next_key.key_name);
if (key_str.length() > 0)
push_string(&item_list, &key_str);
else
item_list.push_back(item_null);
/* `key_len` */
if (key_len_set)
push_string(&item_list, &key_len);
StringBuffer<64> key_len_str;
if (key.key_len != (uint)-1)
{
char buf[64];
size_t length;
length= longlong10_to_str(key.key_len, buf, 10) - buf;
key_len_str.append(buf, length);
if (is_hj && type != JT_HASH)
key_len_str.append(':');
}
if (quick_info)
{
StringBuffer<64> buf2;
quick_info->print_key_len(&buf2);
key_len_str.append(buf2);
}
if (type == JT_HASH_NEXT)
{
char buf[64];
size_t length;
length= longlong10_to_str(hash_next_key.key_len, buf, 10) - buf;
key_len_str.append(buf, length);
}
if (key_len_str.length() > 0)
push_string(&item_list, &key_len_str);
else
item_list.push_back(item_null);
......@@ -416,7 +481,6 @@ int QPF_table_access::print_explain(select_result_sink *output, uint8 explain_fl
extra_buf.append(STRING_WITH_LEN("Using filesort"));
}
const CHARSET_INFO *cs= system_charset_info;
item_list.push_back(new Item_string(extra_buf.ptr(), extra_buf.length(), cs));
if (output->send_data(item_list))
......@@ -476,7 +540,7 @@ void QPF_table_access::append_tag_name(String *str, enum Extra_tag tag)
{
// quick select
str->append(STRING_WITH_LEN("Using "));
str->append(quick_info);
quick_info->print_extra(str);
break;
}
case ET_RANGE_CHECKED_FOR_EACH_RECORD:
......@@ -535,6 +599,127 @@ void QPF_table_access::append_tag_name(String *str, enum Extra_tag tag)
}
/*
This is called for top-level QPF_quick_select only. The point of this
function is:
- index_merge should print $index_merge_type (child, ...)
- 'range' should not print anything.
*/
void QPF_quick_select::print_extra(String *str)
{
if (quick_type == QUICK_SELECT_I::QS_TYPE_RANGE ||
quick_type == QUICK_SELECT_I::QS_TYPE_RANGE_DESC ||
quick_type == QUICK_SELECT_I::QS_TYPE_GROUP_MIN_MAX)
{
/* print nothing */
}
else
print_extra_recursive(str);
}
void QPF_quick_select::print_extra_recursive(String *str)
{
if (quick_type == QUICK_SELECT_I::QS_TYPE_RANGE ||
quick_type == QUICK_SELECT_I::QS_TYPE_RANGE_DESC)
{
str->append(range.key_name);
}
else
{
str->append(get_name_by_type());
str->append('(');
List_iterator_fast<QPF_quick_select> it (children);
QPF_quick_select* child;
bool first= true;
while ((child = it++))
{
if (first)
first= false;
else
str->append(',');
child->print_extra_recursive(str);
}
str->append(')');
}
}
const char * QPF_quick_select::get_name_by_type()
{
switch (quick_type) {
case QUICK_SELECT_I::QS_TYPE_INDEX_MERGE:
return "sort_union";
case QUICK_SELECT_I::QS_TYPE_ROR_UNION:
return "union";
case QUICK_SELECT_I::QS_TYPE_ROR_INTERSECT:
return "intersect";
case QUICK_SELECT_I::QS_TYPE_INDEX_INTERSECT:
return "sort_intersect";
default:
DBUG_ASSERT(0);
return "Oops";
}
}
/*
This prints a comma-separated list of used indexes, ignoring nesting
*/
void QPF_quick_select::print_key(String *str)
{
if (quick_type == QUICK_SELECT_I::QS_TYPE_RANGE ||
quick_type == QUICK_SELECT_I::QS_TYPE_RANGE_DESC ||
quick_type == QUICK_SELECT_I::QS_TYPE_GROUP_MIN_MAX)
{
if (str->length() > 0)
str->append(',');
str->append(range.key_name);
}
else
{
List_iterator_fast<QPF_quick_select> it (children);
QPF_quick_select* child;
while ((child = it++))
{
child->print_key(str);
}
}
}
/*
This prints a comma-separated list of used key_lengths, ignoring nesting
*/
void QPF_quick_select::print_key_len(String *str)
{
if (quick_type == QUICK_SELECT_I::QS_TYPE_RANGE ||
quick_type == QUICK_SELECT_I::QS_TYPE_RANGE_DESC ||
quick_type == QUICK_SELECT_I::QS_TYPE_GROUP_MIN_MAX)
{
char buf[64];
size_t length;
length= longlong10_to_str(range.key_len, buf, 10) - buf;
if (str->length() > 0)
str->append(',');
str->append(buf, length);
}
else
{
List_iterator_fast<QPF_quick_select> it (children);
QPF_quick_select* child;
while ((child = it++))
{
child->print_key_len(str);
}
}
}
int QPF_delete::print_explain(QPF_query *query, select_result_sink *output,
uint8 explain_flags)
{
......
/*
Copyright (c) 2013 Monty Program Ab
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */
/**************************************************************************************
Query Plan Footprint (QPF) structures
......@@ -285,6 +302,41 @@ typedef struct st_qpf_bka_type
} QPF_BKA_TYPE;
/*
Data about how an index is used by some access method
*/
class QPF_index_use : public Sql_alloc
{
public:
const char *key_name;
uint key_len;
/* will add #keyparts here if we implement EXPLAIN FORMAT=JSON */
};
/*
QPF for quick range selects, as well as index_merge select
*/
class QPF_quick_select : public Sql_alloc
{
public:
int quick_type;
/* This is used when quick_type == QUICK_SELECT_I::QS_TYPE_RANGE */
QPF_index_use range;
/* Used in all other cases */
List<QPF_quick_select> children;
void print_extra(String *str);
void print_key(String *str);
void print_key_len(String *str);
private:
void print_extra_recursive(String *str);
const char *get_name_by_type();
};
/*
Query Plan Footprint for a JOIN_TAB.
*/
......@@ -302,29 +354,33 @@ public:
int sjm_nest_select_id;
/* id and 'select_type' are cared-of by the parent QPF_select */
TABLE *table;
StringBuffer<64> table_name;
StringBuffer<32> table_name;
enum join_type type;
StringBuffer<64> used_partitions;
StringBuffer<32> used_partitions;
bool used_partitions_set;
key_map possible_keys;
StringBuffer<64> possible_keys_str;
/* Not used? */
uint key_no;
uint key_length;
bool key_set; /* not set means 'NULL' should be printed */
StringBuffer<64> key;
bool key_len_set; /* not set means 'NULL' should be printed */
StringBuffer<64> key_len;
/* Empty strings means "NULL" will be printed */
StringBuffer<32> possible_keys_str;
/*
Index use: key name and length.
Note: that when one is accessing I_S tables, those may show use of
non-existant indexes.
key.key_name == NULL means 'NULL' will be shown in tabular output.
key.key_len == (uint)-1 means 'NULL' will be shown in tabular output.
*/
QPF_index_use key;
/*
when type==JT_HASH_NEXT, this stores the real index.
*/
QPF_index_use hash_next_key;
bool ref_set; /* not set means 'NULL' should be printed */
StringBuffer<64> ref;
StringBuffer<32> ref;
bool rows_set; /* not set means 'NULL' should be printed */
ha_rows rows;
......@@ -339,7 +395,7 @@ public:
Dynamic_array<enum Extra_tag> extra_tags;
// Valid if ET_USING tag is present
StringBuffer<64> quick_info;
QPF_quick_select *quick_info;
// Valid if ET_USING_INDEX_FOR_GROUP_BY is present
bool loose_scan_is_scanning;
......@@ -348,13 +404,12 @@ public:
key_map range_checked_map;
// valid with ET_USING_MRR
StringBuffer<64> mrr_type;
StringBuffer<32> mrr_type;
// valid with ET_USING_JOIN_BUFFER
QPF_BKA_TYPE bka_type;
//TABLE *firstmatch_table;
StringBuffer<64> firstmatch_table_name;
StringBuffer<32> firstmatch_table_name;
int print_explain(select_result_sink *output, uint8 explain_flags,
uint select_id, const char *select_type,
......
......@@ -11940,78 +11940,106 @@ void QUICK_SELECT_I::add_key_name(String *str, bool *first)
}
void QUICK_RANGE_SELECT::add_info_string(String *str)
void QUICK_RANGE_SELECT::save_info(QPF_quick_select *qpf)
{
bool first= TRUE;
add_key_name(str, &first);
qpf->quick_type= QS_TYPE_RANGE;
qpf->range.key_name= head->key_info[index].name;
qpf->range.key_len= max_used_key_length;
}
void QUICK_GROUP_MIN_MAX_SELECT::save_info(QPF_quick_select *qpf)
{
qpf->quick_type= QS_TYPE_GROUP_MIN_MAX;
qpf->range.key_name= head->key_info[index].name;
qpf->range.key_len= max_used_key_length;
}
void QUICK_INDEX_MERGE_SELECT::add_info_string(String *str)
void QUICK_INDEX_SORT_SELECT::save_info(QPF_quick_select *qpf)
{
qpf->quick_type= get_type();
QUICK_RANGE_SELECT *quick;
bool first= TRUE;
QPF_quick_select *child_qpf;
List_iterator_fast<QUICK_RANGE_SELECT> it(quick_selects);
str->append(STRING_WITH_LEN("sort_union("));
while ((quick= it++))
{
quick->add_key_name(str, &first);
child_qpf= new QPF_quick_select;
qpf->children.push_back(child_qpf);
quick->save_info(child_qpf);
}
if (pk_quick_select)
pk_quick_select->add_key_name(str, &first);
str->append(')');
{
child_qpf= new QPF_quick_select;
qpf->children.push_back(child_qpf);
pk_quick_select->save_info(child_qpf);
}
}
void QUICK_INDEX_INTERSECT_SELECT::add_info_string(String *str)
/*
Same as QUICK_INDEX_SORT_SELECT::save_info(), but primary key is printed
first
*/
void QUICK_INDEX_INTERSECT_SELECT::save_info(QPF_quick_select *qpf)
{
QUICK_RANGE_SELECT *quick;
bool first= TRUE;
List_iterator_fast<QUICK_RANGE_SELECT> it(quick_selects);
qpf->quick_type= get_type();
QPF_quick_select *child_qpf;
str->append(STRING_WITH_LEN("sort_intersect("));
if (pk_quick_select)
pk_quick_select->add_key_name(str, &first);
{
child_qpf= new QPF_quick_select;
qpf->children.push_back(child_qpf);
pk_quick_select->save_info(child_qpf);
}
QUICK_RANGE_SELECT *quick;
List_iterator_fast<QUICK_RANGE_SELECT> it(quick_selects);
while ((quick= it++))
{
quick->add_key_name(str, &first);
child_qpf= new QPF_quick_select;
qpf->children.push_back(child_qpf);
quick->save_info(child_qpf);
}
str->append(')');
}
void QUICK_ROR_INTERSECT_SELECT::add_info_string(String *str)
void QUICK_ROR_INTERSECT_SELECT::save_info(QPF_quick_select *qpf)
{
bool first= TRUE;
qpf->quick_type= get_type();
QUICK_SELECT_WITH_RECORD *qr;
List_iterator_fast<QUICK_SELECT_WITH_RECORD> it(quick_selects);
str->append(STRING_WITH_LEN("intersect("));
while ((qr= it++))
{
qr->quick->add_key_name(str, &first);
QPF_quick_select *child_qpf= new QPF_quick_select;
qpf->children.push_back(child_qpf);
qr->quick->save_info(child_qpf);
}
if (cpk_quick)
cpk_quick->add_key_name(str, &first);
str->append(')');
{
QPF_quick_select *child_qpf= new QPF_quick_select;
qpf->children.push_back(child_qpf);
cpk_quick->save_info(child_qpf);
}
}
void QUICK_ROR_UNION_SELECT::add_info_string(String *str)
void QUICK_ROR_UNION_SELECT::save_info(QPF_quick_select *qpf)
{
qpf->quick_type= get_type();
QUICK_SELECT_I *quick;
bool first= TRUE;
List_iterator_fast<QUICK_SELECT_I> it(quick_selects);
str->append(STRING_WITH_LEN("union("));
while ((quick= it++))
{
if (first)
first= FALSE;
else
str->append(',');
quick->add_info_string(str);
QPF_quick_select *child_qpf= new QPF_quick_select;
qpf->children.push_back(child_qpf);
quick->save_info(child_qpf);
}
str->append(')');
}
......
......@@ -52,7 +52,7 @@ typedef struct st_key_part {
Field::imagetype image_type;
} KEY_PART;
class QPF_quick_select;
/*
A "MIN_TUPLE < tbl.key_tuple < MAX_TUPLE" interval.
......@@ -345,13 +345,8 @@ public:
void add_key_name(String *str, bool *first);
/*
Append text representation of quick select structure (what and how is
merged) to str. The result is added to "Extra" field in EXPLAIN output.
This function is implemented only by quick selects that merge other quick
selects output and/or can produce output suitable for merging.
*/
virtual void add_info_string(String *str) {}
/* Save information about quick select's query plan */
virtual void save_info(QPF_quick_select *qpf)= 0;
/*
Return 1 if any index used by this quick select
......@@ -478,7 +473,7 @@ public:
{ file->position(record); }
int get_type() { return QS_TYPE_RANGE; }
void add_keys_and_lengths(String *key_names, String *used_lengths);
void add_info_string(String *str);
void save_info(QPF_quick_select *qpf);
#ifndef DBUG_OFF
void dbug_dump(int indent, bool verbose);
#endif
......@@ -615,6 +610,7 @@ public:
#ifndef DBUG_OFF
void dbug_dump(int indent, bool verbose);
#endif
void save_info(QPF_quick_select *qpf);
bool push_quick_back(QUICK_RANGE_SELECT *quick_sel_range);
......@@ -663,7 +659,6 @@ public:
int get_next();
int get_type() { return QS_TYPE_INDEX_MERGE; }
void add_keys_and_lengths(String *key_names, String *used_lengths);
void add_info_string(String *str);
};
class QUICK_INDEX_INTERSECT_SELECT : public QUICK_INDEX_SORT_SELECT
......@@ -679,7 +674,7 @@ public:
int get_next();
int get_type() { return QS_TYPE_INDEX_INTERSECT; }
void add_keys_and_lengths(String *key_names, String *used_lengths);
void add_info_string(String *str);
void save_info(QPF_quick_select *qpf);
};
......@@ -717,7 +712,7 @@ public:
bool unique_key_range() { return false; }
int get_type() { return QS_TYPE_ROR_INTERSECT; }
void add_keys_and_lengths(String *key_names, String *used_lengths);
void add_info_string(String *str);
void save_info(QPF_quick_select *qpf);
bool is_keys_used(const MY_BITMAP *fields);
#ifndef DBUG_OFF
void dbug_dump(int indent, bool verbose);
......@@ -796,7 +791,7 @@ public:
bool unique_key_range() { return false; }
int get_type() { return QS_TYPE_ROR_UNION; }
void add_keys_and_lengths(String *key_names, String *used_lengths);
void add_info_string(String *str);
void save_info(QPF_quick_select *qpf);
bool is_keys_used(const MY_BITMAP *fields);
#ifndef DBUG_OFF
void dbug_dump(int indent, bool verbose);
......@@ -945,6 +940,7 @@ public:
#endif
bool is_agg_distinct() { return have_agg_distinct; }
bool loose_scan_is_scanning() { return is_index_scan; }
void save_info(QPF_quick_select *qpf);
};
......
......@@ -11172,7 +11172,8 @@ void JOIN::cleanup(bool full)
if (full)
{
/* Save it again */
#if 0 psergey-todo: remove?
#if 0
psergey-todo: remove?
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 &&
......@@ -22576,20 +22577,12 @@ int JOIN::save_qpf(QPF_query *output, bool need_tmp_table, bool need_order,
TABLE *table=tab->table;
TABLE_LIST *table_list= tab->table->pos_in_table_list;
char buff2[512], buff3[512], buff4[512];
char keylen_str_buf[64];
char buff4[512];
my_bool key_read;
char table_name_buffer[SAFE_NAME_LEN];
String tmp2(buff2,sizeof(buff2),cs);
String tmp3(buff3,sizeof(buff3),cs);
String tmp4(buff4,sizeof(buff4),cs);
char hash_key_prefix[]= "#hash#";
KEY *key_info= 0;
uint key_len= 0;
bool is_hj= tab->type == JT_HASH || tab->type ==JT_HASH_NEXT;
tmp2.length(0);
tmp3.length(0);
tmp4.length(0);
quick_type= -1;
QUICK_SELECT_I *quick= NULL;
......@@ -22611,6 +22604,9 @@ int JOIN::save_qpf(QPF_query *output, bool need_tmp_table, bool need_order,
QPF_table_access *qpt= new (output->mem_root) QPF_table_access;
qp_sel->add_table(qpt);
qpt->key.key_name= NULL;
qpt->key.key_len= (uint)-1;
qpt->quick_info= NULL;
/* id */
if (tab->bush_root_tab)
......@@ -22685,13 +22681,10 @@ int JOIN::save_qpf(QPF_query *output, bool need_tmp_table, bool need_order,
qpt->type= tab_type;
/* Build "possible_keys" value */
qpt->possible_keys= tab->keys;
append_possible_keys(&qpt->possible_keys_str, table, tab->keys);
/* Build "key", "key_len", and "ref" */
// tmp2 holds key_name
// tmp3 holds key_length
// tmp4 holds ref
if (tab_type == JT_NEXT)
{
......@@ -22703,16 +22696,22 @@ int JOIN::save_qpf(QPF_query *output, bool need_tmp_table, bool need_order,
key_info= tab->get_keyinfo_by_key_no(tab->ref.key);
key_len= tab->ref.key_length;
}
/*
In STRAIGHT_JOIN queries, there can be join tabs with JT_CONST type
that still have quick selects.
*/
if (tab->select && tab->select->quick && tab_type != JT_CONST)
{
qpt->quick_info= new QPF_quick_select;
tab->select->quick->save_info(qpt->quick_info);
}
if (key_info)
if (key_info) /* 'index' or 'ref' access */
{
register uint length;
if (is_hj)
tmp2.append(hash_key_prefix, strlen(hash_key_prefix), cs);
tmp2.append(key_info->name, strlen(key_info->name), cs);
length= (longlong10_to_str(key_len, keylen_str_buf, 10) -
keylen_str_buf);
tmp3.append(keylen_str_buf, length, cs);
qpt->key.key_name= key_info->name;
qpt->key.key_len= key_len;
if (tab->ref.key_parts && tab_type != JT_FT)
{
store_key **ref=tab->ref.key_copy;
......@@ -22731,45 +22730,15 @@ int JOIN::save_qpf(QPF_query *output, bool need_tmp_table, bool need_order,
}
}
}
if (is_hj && tab_type != JT_HASH)
{
tmp2.append(':');
tmp3.append(':');
}
if (tab_type == JT_HASH_NEXT)
if (tab_type == JT_HASH_NEXT) /* full index scan + hash join */
{
register uint length;
key_info= table->key_info+tab->index;
key_len= key_info->key_length;
tmp2.append(key_info->name, strlen(key_info->name), cs);
length= (longlong10_to_str(key_len, keylen_str_buf, 10) -
keylen_str_buf);
tmp3.append(keylen_str_buf, length, cs);
qpt->hash_next_key.key_name= table->key_info[tab->index].name;
qpt->hash_next_key.key_len= table->key_info[tab->index].key_length;
}
if (tab->type != JT_CONST && tab->select && quick)
tab->select->quick->add_keys_and_lengths(&tmp2, &tmp3);
if (key_info || (tab->select && quick))
if (key_info)
{
if (tmp2.length())
{
qpt->key.copy(tmp2);
qpt->key_set= true;
}
else
qpt->key_set= false;
if (tmp3.length())
{
qpt->key_len.copy(tmp3);
qpt->key_len_set= true;
}
else
qpt->key_len_set= false;
if (key_info && tab_type != JT_NEXT)
{
qpt->ref.copy(tmp4);
......@@ -22786,37 +22755,37 @@ int JOIN::save_qpf(QPF_query *output, bool need_tmp_table, bool need_order,
{
const char *tmp_buff;
int f_idx;
StringBuffer<64> key_name_buf;
if (table_list->has_db_lookup_value)
{
/* The "key" has the name of the column referring to the database */
f_idx= table_list->schema_table->idx_field1;
tmp_buff= table_list->schema_table->fields_info[f_idx].field_name;
tmp2.append(tmp_buff, strlen(tmp_buff), cs);
key_name_buf.append(tmp_buff, strlen(tmp_buff), cs);
}
if (table_list->has_table_lookup_value)
{
if (table_list->has_db_lookup_value)
tmp2.append(',');
key_name_buf.append(',');
f_idx= table_list->schema_table->idx_field2;
tmp_buff= table_list->schema_table->fields_info[f_idx].field_name;
tmp2.append(tmp_buff, strlen(tmp_buff), cs);
key_name_buf.append(tmp_buff, strlen(tmp_buff), cs);
}
if (tmp2.length())
size_t len;
if ((len= key_name_buf.length()))
{
qpt->key.copy(tmp2);
qpt->key_set= true;
char *ptr= (char*)thd->alloc(len+1);
memcpy(ptr, key_name_buf.c_ptr_safe(), len+1);
qpt->key.key_name= ptr;
qpt->key.key_len= -1;
}
else
qpt->key_set= false;
}
else
qpt->key_set= false;
qpt->key_len_set= false;
qpt->ref_set= false;
}
/* "rows" */
if (table_list /* SJM bushes don't have table_list */ &&
table_list->schema_table)
{
......@@ -22888,7 +22857,6 @@ int JOIN::save_qpf(QPF_query *output, bool need_tmp_table, bool need_order,
quick_type == QUICK_SELECT_I::QS_TYPE_INDEX_MERGE)
{
qpt->push_extra(ET_USING);
tab->select->quick->add_info_string(&qpt->quick_info);
}
if (tab->select)
{
......
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