MDEV-5816: Stored programs: validation of stored program statements
Added re-parsing of failed statements inside a stored routine. General idea of the patch is to install an instance of the class Reprepare_observer before executing a next SP instruction and re-parse a statement of this SP instruction in case of its execution failure. To implement the described approach the class sp_lex_keeper has been extended with the method validate_lex_and_exec_core() that is just a wrapper around the method reset_lex_and_exec_core() with additional setting/resetting an instance of the class Reprepare_observer on each iteration of SP instruction execution. If reset_lex_and_exec_core() returns error and an instance of the class Reprepare_observer is installed before running a SP instruction then a number of attempts to re-run the SP instruction is checked against a max. limit and in case it doesn't reach the limit a statement for the failed SP instruction is re-parsed. Re-parsing of a statement for the failed SP instruction is implemented by the new method sp_le_inst::parse_expr() that prepends a SP instruction's statement with the clause 'SELECT' and parse it. Own SP instruction MEM_ROOT and a separate free_list is used for parsing of a SP statement. On successful re-parsing of SP instruction's statement the virtual methods adjust_sql_command() and on_after_expr_parsing() of the class sp_lex_instr is called to update the SP instruction state with a new data created on parsing the statement. Few words about reason for prepending a SP instruction's statement with the clause 'SELECT' - this is required step to produce a valid SQL statement, since for some SP instructions the instructions statement is not a valid SQL statement. Wrapping such text into 'SELECT ( )' produces a correct operator from SQL syntax point of view.
This commit is contained in:
parent
5a8b9a16d1
commit
465c81b323
63
sql/sp.cc
63
sql/sp.cc
@ -831,6 +831,45 @@ Silence_deprecated_warning::handle_condition(
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Make a copy of a SQL statement used for creation of a stored routine.
|
||||
|
||||
@param defstr Original SQL statement that is used for creation
|
||||
a stored routine
|
||||
@param sp_mem_root Memory root where a copy of original SQL statement should
|
||||
be placed.
|
||||
|
||||
@return a copy of an original CREATE PROCEDURE/FUNCTION/EVENT/TRIGGER
|
||||
SQL statement wrapped into an instance of LEX_STRING.
|
||||
The data member LEX_STRING.str of returning object is set nullptr
|
||||
in case of error.
|
||||
*/
|
||||
|
||||
static LEX_STRING copy_definition_string(String *defstr,
|
||||
MEM_ROOT *sp_mem_root)
|
||||
{
|
||||
LEX_STRING definition_string;
|
||||
|
||||
/*
|
||||
Make a \0-terminated copy of the original SQL statement
|
||||
*/
|
||||
definition_string.str= (char*)strmake_root(sp_mem_root, defstr->c_ptr_safe(),
|
||||
defstr->length());
|
||||
if (!definition_string.str)
|
||||
{
|
||||
my_error(ER_OUTOFMEMORY, MYF(ME_FATAL), defstr->length());
|
||||
return LEX_STRING{nullptr, 0};
|
||||
}
|
||||
|
||||
/*
|
||||
Set the length as an original string has
|
||||
*/
|
||||
definition_string.length= defstr->length();
|
||||
|
||||
return definition_string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@brief The function parses input strings and returns SP stucture.
|
||||
|
||||
@ -860,14 +899,33 @@ static sp_head *sp_compile(THD *thd, String *defstr, sql_mode_t sql_mode,
|
||||
thd->variables.sql_mode= sql_mode;
|
||||
thd->variables.select_limit= HA_POS_ERROR;
|
||||
|
||||
if (parser_state.init(thd, defstr->c_ptr_safe(), defstr->length()))
|
||||
LEX_STRING definition_string;
|
||||
|
||||
lex_start(thd);
|
||||
|
||||
init_sql_alloc(key_memory_sp_head_main_root, &thd->lex->sp_mem_root,
|
||||
MEM_ROOT_BLOCK_SIZE, MEM_ROOT_PREALLOC, MYF(0));
|
||||
|
||||
thd->lex->sp_mem_root_ptr= &thd->lex->sp_mem_root;
|
||||
/*
|
||||
Copy a stored routine definition string to a memory buffer allocated on
|
||||
the stored routine's memory root.
|
||||
*/
|
||||
definition_string= copy_definition_string(defstr, thd->lex->sp_mem_root_ptr);
|
||||
|
||||
/*
|
||||
Check for OOM condition
|
||||
*/
|
||||
if (!definition_string.str)
|
||||
return nullptr;
|
||||
|
||||
if (parser_state.init(thd, definition_string.str, definition_string.length))
|
||||
{
|
||||
thd->variables.sql_mode= old_sql_mode;
|
||||
thd->variables.select_limit= old_select_limit;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
lex_start(thd);
|
||||
thd->lex->sphead= parent;
|
||||
thd->push_internal_handler(&warning_handler);
|
||||
thd->spcont= 0;
|
||||
@ -881,6 +939,7 @@ static sp_head *sp_compile(THD *thd, String *defstr, sql_mode_t sql_mode,
|
||||
else
|
||||
{
|
||||
sp= thd->lex->sphead;
|
||||
sp->set_definition_string(definition_string);
|
||||
}
|
||||
|
||||
thd->pop_internal_handler();
|
||||
|
@ -511,16 +511,16 @@ check_routine_name(const LEX_CSTRING *ident)
|
||||
*/
|
||||
|
||||
sp_head *sp_head::create(sp_package *parent, const Sp_handler *handler,
|
||||
enum_sp_aggregate_type agg_type)
|
||||
enum_sp_aggregate_type agg_type, MEM_ROOT *sp_mem_root)
|
||||
{
|
||||
MEM_ROOT own_root;
|
||||
init_sql_alloc(key_memory_sp_head_main_root, &own_root, MEM_ROOT_BLOCK_SIZE,
|
||||
MEM_ROOT_PREALLOC, MYF(0));
|
||||
sp_head *sp;
|
||||
if (!(sp= new (&own_root) sp_head(&own_root, parent, handler, agg_type)))
|
||||
free_root(&own_root, MYF(0));
|
||||
|
||||
return sp;
|
||||
if (!sp_mem_root)
|
||||
{
|
||||
init_sql_alloc(key_memory_sp_head_main_root, &own_root, MEM_ROOT_BLOCK_SIZE,
|
||||
MEM_ROOT_PREALLOC, MYF(0));
|
||||
sp_mem_root= &own_root;
|
||||
}
|
||||
return new (sp_mem_root) sp_head(sp_mem_root, parent, handler, agg_type);
|
||||
}
|
||||
|
||||
|
||||
@ -534,7 +534,6 @@ void sp_head::destroy(sp_head *sp)
|
||||
&sp->mem_root, &own_root));
|
||||
delete sp;
|
||||
|
||||
|
||||
free_root(&own_root, MYF(0));
|
||||
}
|
||||
}
|
||||
@ -881,7 +880,6 @@ sp_head::set_stmt_end(THD *thd)
|
||||
|
||||
sp_head::~sp_head()
|
||||
{
|
||||
LEX *lex;
|
||||
sp_instr *i;
|
||||
DBUG_ENTER("sp_head::~sp_head");
|
||||
|
||||
@ -900,14 +898,7 @@ sp_head::~sp_head()
|
||||
THD::lex. It is safe to not update LEX::ptr because further query
|
||||
string parsing and execution will be stopped anyway.
|
||||
*/
|
||||
while ((lex= (LEX *)m_lex.pop()))
|
||||
{
|
||||
THD *thd= lex->thd;
|
||||
thd->lex->sphead= NULL;
|
||||
lex_end(thd->lex);
|
||||
delete thd->lex;
|
||||
thd->lex= lex;
|
||||
}
|
||||
unwind_aux_lexes_and_restore_original_lex();
|
||||
|
||||
my_hash_free(&m_sptabs);
|
||||
my_hash_free(&m_sroutines);
|
||||
@ -2513,6 +2504,22 @@ sp_head::merge_lex(THD *thd, LEX *oldlex, LEX *sublex)
|
||||
DBUG_RETURN(FALSE);
|
||||
}
|
||||
|
||||
|
||||
void sp_head::unwind_aux_lexes_and_restore_original_lex()
|
||||
{
|
||||
LEX *lex;
|
||||
|
||||
while ((lex= (LEX *)m_lex.pop()))
|
||||
{
|
||||
THD *thd= lex->thd;
|
||||
thd->lex->sphead= NULL;
|
||||
lex_end(thd->lex);
|
||||
delete thd->lex;
|
||||
thd->lex= lex;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Put the instruction on the backpatch list, associated with the label.
|
||||
*/
|
||||
|
@ -330,7 +330,8 @@ protected:
|
||||
public:
|
||||
static void destroy(sp_head *sp);
|
||||
static sp_head *create(sp_package *parent, const Sp_handler *handler,
|
||||
enum_sp_aggregate_type agg_type);
|
||||
enum_sp_aggregate_type agg_type,
|
||||
MEM_ROOT *sp_mem_root);
|
||||
|
||||
/// Initialize after we have reset mem_root
|
||||
void
|
||||
@ -632,6 +633,16 @@ public:
|
||||
DBUG_RETURN(false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Delete all auxiliary LEX objects created on parsing a statement and
|
||||
restore a value of the data member THD::lex to point on the LEX object
|
||||
that was actual before parsing started.
|
||||
*/
|
||||
|
||||
void unwind_aux_lexes_and_restore_original_lex();
|
||||
|
||||
|
||||
/**
|
||||
Iterate through the LEX stack from the top (the newest) to the bottom
|
||||
(the oldest) and find the one that contains a non-zero spname.
|
||||
@ -792,6 +803,10 @@ public:
|
||||
m_definer.copy(mem_root, user_name, host_name);
|
||||
}
|
||||
|
||||
void set_definition_string(LEX_STRING &defstr)
|
||||
{
|
||||
m_definition_string= defstr;
|
||||
}
|
||||
void reset_thd_mem_root(THD *thd);
|
||||
|
||||
void restore_thd_mem_root(THD *thd);
|
||||
@ -937,6 +952,12 @@ protected:
|
||||
*/
|
||||
HASH m_sptabs;
|
||||
|
||||
/**
|
||||
Text of the query CREATE PROCEDURE/FUNCTION/TRIGGER/EVENT ...
|
||||
used for DDL parsing.
|
||||
*/
|
||||
LEX_STRING m_definition_string;
|
||||
|
||||
bool
|
||||
execute(THD *thd, bool merge_da_on_success);
|
||||
|
||||
@ -966,6 +987,15 @@ public:
|
||||
being opened is probably enough).
|
||||
*/
|
||||
SQL_I_List<Item_trigger_field> m_trg_table_fields;
|
||||
|
||||
/**
|
||||
The object of the Trigger class corresponding to this sp_head object.
|
||||
This data member is set on table's triggers loading at the function
|
||||
check_n_load and is used at the method sp_lex_instr::parse_expr
|
||||
for accessing to the trigger's table after re-parsing of failed
|
||||
trigger's instruction.
|
||||
*/
|
||||
Trigger *m_trg= nullptr;
|
||||
}; // class sp_head : public Sql_alloc
|
||||
|
||||
|
||||
|
285
sql/sp_instr.cc
285
sql/sp_instr.cc
@ -361,6 +361,115 @@ sp_lex_keeper::reset_lex_and_exec_core(THD *thd, uint *nextp,
|
||||
}
|
||||
|
||||
|
||||
void sp_lex_keeper::free_lex(THD *thd)
|
||||
{
|
||||
if (!m_lex_resp || !m_lex) return;
|
||||
|
||||
/* Prevent endless recursion. */
|
||||
m_lex->sphead= nullptr;
|
||||
lex_end(m_lex);
|
||||
|
||||
delete (st_lex_local *)m_lex;
|
||||
|
||||
/*
|
||||
Set thd->lex to the null value in case it points to a LEX object
|
||||
we just deleted in order to avoid dangling pointer problem
|
||||
*/
|
||||
if (thd->lex == m_lex)
|
||||
thd->lex= nullptr;
|
||||
|
||||
m_lex= nullptr;
|
||||
m_lex_resp= false;
|
||||
lex_query_tables_own_last= nullptr;
|
||||
}
|
||||
|
||||
|
||||
void sp_lex_keeper::set_lex(LEX *lex, bool is_lex_owner)
|
||||
{
|
||||
m_lex= lex;
|
||||
m_lex_resp= is_lex_owner;
|
||||
|
||||
if (m_lex)
|
||||
m_lex->sp_lex_in_use= true;
|
||||
}
|
||||
|
||||
|
||||
int sp_lex_keeper::validate_lex_and_exec_core(THD *thd, uint *nextp,
|
||||
bool open_tables,
|
||||
sp_lex_instr* instr)
|
||||
{
|
||||
Reprepare_observer reprepare_observer;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (instr->is_invalid())
|
||||
{
|
||||
thd->clear_error();
|
||||
free_lex(thd);
|
||||
LEX *lex= instr->parse_expr(thd, thd->spcont->m_sp);
|
||||
|
||||
if (!lex) return true;
|
||||
|
||||
set_lex(lex, true);
|
||||
|
||||
m_first_execution= true;
|
||||
}
|
||||
|
||||
Reprepare_observer *stmt_reprepare_observer= nullptr;
|
||||
|
||||
if (!m_first_execution &&
|
||||
((sql_command_flags[m_lex->sql_command] & CF_REEXECUTION_FRAGILE) ||
|
||||
m_lex->sql_command == SQLCOM_END))
|
||||
{
|
||||
reprepare_observer.reset_reprepare_observer();
|
||||
stmt_reprepare_observer= &reprepare_observer;
|
||||
}
|
||||
|
||||
Reprepare_observer *save_reprepare_observer= thd->m_reprepare_observer;
|
||||
thd->m_reprepare_observer= stmt_reprepare_observer;
|
||||
|
||||
bool rc= reset_lex_and_exec_core(thd, nextp, open_tables, instr);
|
||||
|
||||
thd->m_reprepare_observer= save_reprepare_observer;
|
||||
|
||||
m_first_execution= false;
|
||||
|
||||
if (!rc)
|
||||
break;
|
||||
|
||||
/*
|
||||
Raise the error upper level in case:
|
||||
- we got an error and Reprepare_observer is not set
|
||||
- a fatal error has been got
|
||||
- the current execution thread has been killed
|
||||
- an error different from ER_NEED_REPREPARE has been got.
|
||||
*/
|
||||
if (stmt_reprepare_observer == nullptr ||
|
||||
thd->is_fatal_error ||
|
||||
thd->killed ||
|
||||
thd->get_stmt_da()->get_sql_errno() != ER_NEED_REPREPARE)
|
||||
return 1;
|
||||
|
||||
if (!stmt_reprepare_observer->can_retry())
|
||||
{
|
||||
/*
|
||||
Reprepare_observer sets error status in DA but Sql_condition is not
|
||||
added. Please check Reprepare_observer::report_error(). Pushing
|
||||
Sql_condition for ER_NEED_REPREPARE here.
|
||||
*/
|
||||
Diagnostics_area *da= thd->get_stmt_da();
|
||||
da->push_warning(thd, da->get_sql_errno(), da->get_sqlstate(),
|
||||
Sql_state_errno_level::WARN_LEVEL_ERROR, da->message());
|
||||
return 1;
|
||||
}
|
||||
|
||||
instr->invalidate();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int sp_lex_keeper::cursor_reset_lex_and_exec_core(THD *thd, uint *nextp,
|
||||
bool open_tables,
|
||||
sp_instr *instr)
|
||||
@ -379,7 +488,6 @@ int sp_lex_keeper::cursor_reset_lex_and_exec_core(THD *thd, uint *nextp,
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
sp_instr class functions
|
||||
*/
|
||||
@ -438,6 +546,148 @@ void sp_lex_instr::get_query(String *sql_query) const
|
||||
}
|
||||
|
||||
|
||||
void sp_lex_instr::cleanup_before_parsing()
|
||||
{
|
||||
Item *current= free_list;
|
||||
|
||||
while (current)
|
||||
{
|
||||
Item *next= current->next;
|
||||
current->delete_self();
|
||||
current= next;
|
||||
}
|
||||
|
||||
free_list= nullptr;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Set up field object for every NEW/OLD item of the trigger.
|
||||
|
||||
@param thd current thread
|
||||
@param sp sp_head object of the trigger
|
||||
*/
|
||||
|
||||
static void setup_table_fields_for_trigger(THD *thd, sp_head *sp)
|
||||
{
|
||||
DBUG_ASSERT(sp->m_trg);
|
||||
|
||||
for (Item_trigger_field *trg_field= sp->m_trg_table_fields.first;
|
||||
trg_field;
|
||||
trg_field= trg_field->next_trg_field)
|
||||
{
|
||||
trg_field->setup_field(thd, sp->m_trg->base->get_subject_table(),
|
||||
&sp->m_trg->subject_table_grants);
|
||||
}
|
||||
}
|
||||
|
||||
LEX* sp_lex_instr::parse_expr(THD *thd, sp_head *sp)
|
||||
{
|
||||
String sql_query;
|
||||
|
||||
get_query(&sql_query);
|
||||
|
||||
if (sql_query.length() == 0)
|
||||
{
|
||||
/**
|
||||
The instruction has returned zero-length query string. That means, the
|
||||
re-preparation of the instruction is not possible. We should not come
|
||||
here in the normal case.
|
||||
*/
|
||||
assert(false);
|
||||
my_error(ER_UNKNOWN_ERROR, MYF(0));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
cleanup_before_parsing();
|
||||
|
||||
Parser_state parser_state;
|
||||
|
||||
if (parser_state.init(thd, sql_query.c_ptr(), sql_query.length()))
|
||||
return nullptr;
|
||||
|
||||
// Create a new LEX and initialize it.
|
||||
|
||||
LEX *lex_saved= thd->lex;
|
||||
|
||||
DBUG_ASSERT(mem_root != thd->mem_root);
|
||||
/*
|
||||
Back up the current free_list pointer and reset it to nullptr.
|
||||
Set thd->mem_root pointing to a mem_root of SP instruction being re-parsed.
|
||||
In that way any items created on parsing a statement of the current
|
||||
instruction is allocated on SP instruction's mem_root and placed on its own
|
||||
free_list that later assigned to the current sp_instr. We use the separate
|
||||
free list for every instruction since at least at one place in the source
|
||||
code (the function subst_spvars() to be accurate) we iterate along the
|
||||
list sp_instr->free_list on executing of every SP instruction.
|
||||
*/
|
||||
Query_arena backup;
|
||||
/*
|
||||
A statement of SP instruction is going to be re-parsed, so reset
|
||||
SP arena's state to STMT_INITIALIZED_FOR_SP as its initial state.
|
||||
*/
|
||||
state= STMT_INITIALIZED_FOR_SP;
|
||||
thd->set_n_backup_active_arena(this, &backup);
|
||||
thd->free_list= nullptr;
|
||||
|
||||
thd->lex= new (thd->mem_root) st_lex_local;
|
||||
|
||||
lex_start(thd);
|
||||
|
||||
thd->lex->sphead= sp;
|
||||
thd->lex->spcont= m_ctx;
|
||||
|
||||
sql_digest_state *parent_digest= thd->m_digest;
|
||||
PSI_statement_locker *parent_locker= thd->m_statement_psi;
|
||||
|
||||
thd->m_digest= nullptr;
|
||||
thd->m_statement_psi= nullptr;
|
||||
|
||||
/*
|
||||
sp_head::m_tmp_query is set by parser on parsing every statement of
|
||||
a stored routine. Since here we re-parse failed statement outside stored
|
||||
routine context, this data member isn't set. In result, the assert
|
||||
DBUG_ASSERT(sphead->m_tmp_query <= start)
|
||||
is fired in the constructor of the class Query_fragment.
|
||||
To fix the assert failure, reset this data member to point to beginning of
|
||||
the current statement being parsed.
|
||||
*/
|
||||
const char *m_tmp_query_bak= sp->m_tmp_query;
|
||||
sp->m_tmp_query= sql_query.c_ptr();
|
||||
|
||||
bool parsing_failed= parse_sql(thd, &parser_state, nullptr);
|
||||
|
||||
sp->m_tmp_query= m_tmp_query_bak;
|
||||
thd->m_digest= parent_digest;
|
||||
thd->m_statement_psi= parent_locker;
|
||||
|
||||
if (!parsing_failed)
|
||||
{
|
||||
thd->lex->set_trg_event_type_for_tables();
|
||||
adjust_sql_command(thd->lex);
|
||||
parsing_failed= on_after_expr_parsing(thd);
|
||||
|
||||
if (sp->m_handler->type() == SP_TYPE_TRIGGER)
|
||||
setup_table_fields_for_trigger(thd, sp);
|
||||
|
||||
/*
|
||||
Assign the list of items created on parsing to the current
|
||||
stored routine instruction.
|
||||
*/
|
||||
free_list= thd->free_list;
|
||||
thd->free_list= nullptr;
|
||||
}
|
||||
|
||||
Query_arena old;
|
||||
thd->restore_active_arena(&old, &backup);
|
||||
|
||||
LEX *expr_lex= thd->lex;
|
||||
thd->lex= lex_saved;
|
||||
|
||||
return parsing_failed ? nullptr : expr_lex;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
sp_instr_stmt class functions
|
||||
*/
|
||||
@ -478,7 +728,7 @@ sp_instr_stmt::execute(THD *thd, uint *nextp)
|
||||
thd->query_length()) <= 0)
|
||||
{
|
||||
thd->reset_slow_query_state();
|
||||
res= m_lex_keeper.reset_lex_and_exec_core(thd, nextp, false, this);
|
||||
res= m_lex_keeper.validate_lex_and_exec_core(thd, nextp, false, this);
|
||||
bool log_slow= !res && thd->enable_slow_log;
|
||||
|
||||
/* Finalize server status flags after executing a statement. */
|
||||
@ -592,7 +842,7 @@ sp_instr_set::execute(THD *thd, uint *nextp)
|
||||
DBUG_ENTER("sp_instr_set::execute");
|
||||
DBUG_PRINT("info", ("offset: %u", m_offset));
|
||||
|
||||
DBUG_RETURN(m_lex_keeper.reset_lex_and_exec_core(thd, nextp, true, this));
|
||||
DBUG_RETURN(m_lex_keeper.validate_lex_and_exec_core(thd, nextp, true, this));
|
||||
}
|
||||
|
||||
|
||||
@ -744,7 +994,7 @@ sp_instr_set_trigger_field::execute(THD *thd, uint *nextp)
|
||||
{
|
||||
DBUG_ENTER("sp_instr_set_trigger_field::execute");
|
||||
thd->count_cuted_fields= CHECK_FIELD_ERROR_FOR_NULL;
|
||||
DBUG_RETURN(m_lex_keeper.reset_lex_and_exec_core(thd, nextp, true, this));
|
||||
DBUG_RETURN(m_lex_keeper.validate_lex_and_exec_core(thd, nextp, true, this));
|
||||
}
|
||||
|
||||
|
||||
@ -836,6 +1086,27 @@ sp_instr_jump::opt_move(uint dst, List<sp_instr_opt_meta> *bp)
|
||||
m_ip= dst;
|
||||
}
|
||||
|
||||
bool sp_instr_set_trigger_field::on_after_expr_parsing(THD *thd)
|
||||
{
|
||||
DBUG_ASSERT(thd->lex->current_select->item_list.elements == 1);
|
||||
|
||||
value= thd->lex->current_select->item_list.head();
|
||||
DBUG_ASSERT(value != nullptr);
|
||||
|
||||
trigger_field = new (thd->mem_root)
|
||||
Item_trigger_field(thd, thd->lex->current_context(),
|
||||
Item_trigger_field::NEW_ROW,
|
||||
m_trigger_field_name, UPDATE_ACL, false);
|
||||
|
||||
if (!value || !trigger_field)
|
||||
return true;
|
||||
|
||||
thd->spcont->m_sp->m_trg_table_fields.link_in_list(
|
||||
trigger_field, &trigger_field->next_trg_field);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
sp_instr_jump_if_not class functions
|
||||
@ -849,7 +1120,7 @@ sp_instr_jump_if_not::execute(THD *thd, uint *nextp)
|
||||
{
|
||||
DBUG_ENTER("sp_instr_jump_if_not::execute");
|
||||
DBUG_PRINT("info", ("destination: %u", m_dest));
|
||||
DBUG_RETURN(m_lex_keeper.reset_lex_and_exec_core(thd, nextp, true, this));
|
||||
DBUG_RETURN(m_lex_keeper.validate_lex_and_exec_core(thd, nextp, true, this));
|
||||
}
|
||||
|
||||
|
||||
@ -953,7 +1224,7 @@ int
|
||||
sp_instr_freturn::execute(THD *thd, uint *nextp)
|
||||
{
|
||||
DBUG_ENTER("sp_instr_freturn::execute");
|
||||
DBUG_RETURN(m_lex_keeper.reset_lex_and_exec_core(thd, nextp, true, this));
|
||||
DBUG_RETURN(m_lex_keeper.validate_lex_and_exec_core(thd, nextp, true, this));
|
||||
}
|
||||
|
||||
|
||||
@ -1610,7 +1881,7 @@ sp_instr_set_case_expr::execute(THD *thd, uint *nextp)
|
||||
{
|
||||
DBUG_ENTER("sp_instr_set_case_expr::execute");
|
||||
|
||||
DBUG_RETURN(m_lex_keeper.reset_lex_and_exec_core(thd, nextp, true, this));
|
||||
DBUG_RETURN(m_lex_keeper.validate_lex_and_exec_core(thd, nextp, true, this));
|
||||
}
|
||||
|
||||
|
||||
|
183
sql/sp_instr.h
183
sql/sp_instr.h
@ -201,6 +201,9 @@ public:
|
||||
}; // class sp_instr : public Sql_alloc
|
||||
|
||||
|
||||
class sp_instr;
|
||||
class sp_lex_instr;
|
||||
|
||||
/**
|
||||
Auxilary class to which instructions delegate responsibility
|
||||
for handling LEX and preparations before executing statement
|
||||
@ -225,7 +228,8 @@ public:
|
||||
: m_lex(lex),
|
||||
m_lex_resp(lex_resp),
|
||||
prelocking_tables(nullptr),
|
||||
lex_query_tables_own_last(nullptr)
|
||||
lex_query_tables_own_last(nullptr),
|
||||
m_first_execution(true)
|
||||
{
|
||||
lex->sp_lex_in_use= true;
|
||||
}
|
||||
@ -251,9 +255,43 @@ public:
|
||||
int reset_lex_and_exec_core(THD *thd, uint *nextp, bool open_tables,
|
||||
sp_instr* instr);
|
||||
|
||||
|
||||
/**
|
||||
Do several attempts to execute an instruction.
|
||||
|
||||
This method installs Reprepare_observer to catch possible metadata changes
|
||||
on depending database objects, then calls reset_lex_and_exec_core()
|
||||
to execute the instruction. If execution of the instruction fails, does
|
||||
re-parsing of the instruction and re-execute it.
|
||||
|
||||
@param thd Thread context.
|
||||
@param[out] nextp Pointer for storing a next instruction to execute
|
||||
@param open_tables Flag to specify if the function should check read
|
||||
access to tables in LEX's table list and open and
|
||||
lock them (used in instructions which need to
|
||||
calculate some expression and don't execute
|
||||
complete statement).
|
||||
@param instr instruction which we prepare context and run.
|
||||
|
||||
@return 0 on success, 1 on error
|
||||
*/
|
||||
int validate_lex_and_exec_core(THD *thd, uint *nextp, bool open_tables,
|
||||
sp_lex_instr* instr);
|
||||
|
||||
int cursor_reset_lex_and_exec_core(THD *thd, uint *nextp, bool open_tables,
|
||||
sp_instr *instr);
|
||||
|
||||
/**
|
||||
(Re-)parse the query corresponding to this instruction and return a new
|
||||
LEX-object.
|
||||
|
||||
@param thd Thread context.
|
||||
@param sp The stored program.
|
||||
|
||||
@return new LEX-object or NULL in case of failure.
|
||||
*/
|
||||
LEX *parse_expr(THD *thd, const sp_head *sp);
|
||||
|
||||
inline uint sql_command() const
|
||||
{
|
||||
return (uint)m_lex->sql_command;
|
||||
@ -264,6 +302,22 @@ public:
|
||||
m_lex->safe_to_cache_query= 0;
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
Clean up and destroy owned LEX object.
|
||||
*/
|
||||
void free_lex(THD *thd);
|
||||
|
||||
/**
|
||||
Set LEX object.
|
||||
|
||||
@param lex LEX-object
|
||||
@param is_lex_owner this flag specifies whether this LEX object is owned
|
||||
by the sp_lex_keeper and so should deleted when
|
||||
needed.
|
||||
*/
|
||||
void set_lex(LEX *lex, bool is_lex_owner);
|
||||
|
||||
private:
|
||||
LEX *m_lex;
|
||||
|
||||
@ -291,6 +345,8 @@ private:
|
||||
statement enters/leaves prelocked mode on its own.
|
||||
*/
|
||||
TABLE_LIST **lex_query_tables_own_last;
|
||||
|
||||
bool m_first_execution;
|
||||
};
|
||||
|
||||
|
||||
@ -319,6 +375,18 @@ public:
|
||||
*/
|
||||
virtual void get_query(String *sql_query) const;
|
||||
|
||||
|
||||
/**
|
||||
(Re-)parse the query corresponding to this instruction and return a new
|
||||
LEX-object.
|
||||
|
||||
@param thd Thread context.
|
||||
@param sp The stored program.
|
||||
|
||||
@return new LEX-object or NULL in case of failure.
|
||||
*/
|
||||
LEX *parse_expr(THD *thd, sp_head *sp);
|
||||
|
||||
protected:
|
||||
/**
|
||||
@return the expression query string. This string can't be passed directly
|
||||
@ -326,7 +394,36 @@ protected:
|
||||
*/
|
||||
virtual LEX_CSTRING get_expr_query() const = 0;
|
||||
|
||||
/**
|
||||
Some expressions may be re-parsed as SELECT statements.
|
||||
This method is overridden in derived classes for instructions
|
||||
those SQL command should be adjusted.
|
||||
*/
|
||||
virtual void adjust_sql_command(LEX *)
|
||||
{}
|
||||
|
||||
/**
|
||||
Callback method which is called after an expression string successfully
|
||||
parsed and the thread context has not been switched to the outer context.
|
||||
The thread context contains new LEX-object corresponding to the parsed
|
||||
expression string.
|
||||
|
||||
@param thd Thread context.
|
||||
|
||||
@return Error flag.
|
||||
*/
|
||||
virtual bool on_after_expr_parsing(THD *)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
sp_lex_keeper m_lex_keeper;
|
||||
|
||||
private:
|
||||
/**
|
||||
Clean up items previously created on behalf of the current instruction.
|
||||
*/
|
||||
void cleanup_before_parsing();
|
||||
};
|
||||
|
||||
|
||||
@ -383,6 +480,13 @@ protected:
|
||||
return LEX_CSTRING{m_query.str, m_query.length};
|
||||
}
|
||||
|
||||
protected:
|
||||
bool on_after_expr_parsing(THD *) override
|
||||
{
|
||||
m_valid= true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public:
|
||||
PSI_statement_info* get_psi_info() override { return & psi_info; }
|
||||
static PSI_statement_info psi_info;
|
||||
@ -431,6 +535,23 @@ protected:
|
||||
return m_expr_str;
|
||||
}
|
||||
|
||||
void adjust_sql_command(LEX *lex) override
|
||||
{
|
||||
DBUG_ASSERT(lex->sql_command == SQLCOM_SELECT);
|
||||
lex->sql_command= SQLCOM_SET_OPTION;
|
||||
}
|
||||
|
||||
bool on_after_expr_parsing(THD *thd) override
|
||||
{
|
||||
DBUG_ASSERT(thd->lex->current_select->item_list.elements == 1);
|
||||
|
||||
m_value= thd->lex->current_select->item_list.head();
|
||||
DBUG_ASSERT(m_value != nullptr);
|
||||
|
||||
// Return error in release version if m_value == nullptr
|
||||
return m_value == nullptr;
|
||||
}
|
||||
|
||||
sp_rcontext *get_rcontext(THD *thd) const;
|
||||
const Sp_rcontext_handler *m_rcontext_handler;
|
||||
uint m_offset; ///< Frame offset
|
||||
@ -538,7 +659,11 @@ public:
|
||||
trigger_field(trg_fld),
|
||||
value(val),
|
||||
m_expr_str(value_query)
|
||||
{}
|
||||
{
|
||||
m_trigger_field_name=
|
||||
LEX_CSTRING{strdup_root(current_thd->mem_root, trg_fld->field_name.str),
|
||||
trg_fld->field_name.length};
|
||||
}
|
||||
|
||||
virtual ~sp_instr_set_trigger_field() = default;
|
||||
|
||||
@ -564,6 +689,8 @@ protected:
|
||||
return m_expr_str;
|
||||
}
|
||||
|
||||
bool on_after_expr_parsing(THD *thd) override;
|
||||
|
||||
private:
|
||||
Item_trigger_field *trigger_field;
|
||||
Item *value;
|
||||
@ -572,6 +699,8 @@ private:
|
||||
*/
|
||||
LEX_CSTRING m_expr_str;
|
||||
|
||||
LEX_CSTRING m_trigger_field_name;
|
||||
|
||||
public:
|
||||
PSI_statement_info* get_psi_info() override { return & psi_info; }
|
||||
static PSI_statement_info psi_info;
|
||||
@ -741,6 +870,23 @@ protected:
|
||||
return m_expr_str;
|
||||
}
|
||||
|
||||
void adjust_sql_command(LEX *lex) override
|
||||
{
|
||||
assert(lex->sql_command == SQLCOM_SELECT);
|
||||
lex->sql_command= SQLCOM_END;
|
||||
}
|
||||
|
||||
bool on_after_expr_parsing(THD *thd) override
|
||||
{
|
||||
DBUG_ASSERT(thd->lex->current_select->item_list.elements == 1);
|
||||
|
||||
m_expr= thd->lex->current_select->item_list.head();
|
||||
DBUG_ASSERT(m_expr != nullptr);
|
||||
|
||||
// Return error in release version if m_expr == nullptr
|
||||
return m_expr == nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
Item *m_expr; ///< The condition
|
||||
LEX_CSTRING m_expr_str;
|
||||
@ -823,6 +969,16 @@ protected:
|
||||
return m_expr_str;
|
||||
}
|
||||
|
||||
bool on_after_expr_parsing(THD *thd) override
|
||||
{
|
||||
DBUG_ASSERT(thd->lex->current_select->item_list.elements == 1);
|
||||
m_value= thd->lex->current_select->item_list.head();
|
||||
DBUG_ASSERT(m_value != nullptr);
|
||||
|
||||
// Return error in release version if m_value == nullptr
|
||||
return m_value == nullptr;
|
||||
}
|
||||
|
||||
Item *m_value;
|
||||
const Type_handler *m_type_handler;
|
||||
|
||||
@ -1161,6 +1317,12 @@ protected:
|
||||
return m_cursor_stmt;
|
||||
}
|
||||
|
||||
bool on_after_expr_parsing(THD *) override
|
||||
{
|
||||
m_valid= true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public:
|
||||
PSI_statement_info* get_psi_info() override { return & psi_info; }
|
||||
static PSI_statement_info psi_info;
|
||||
@ -1341,6 +1503,23 @@ protected:
|
||||
return m_expr_str;
|
||||
}
|
||||
|
||||
void adjust_sql_command(LEX *lex) override
|
||||
{
|
||||
assert(lex->sql_command == SQLCOM_SELECT);
|
||||
lex->sql_command= SQLCOM_END;
|
||||
}
|
||||
|
||||
bool on_after_expr_parsing(THD *thd) override
|
||||
{
|
||||
DBUG_ASSERT(thd->lex->current_select->item_list.elements == 1);
|
||||
|
||||
m_case_expr= thd->lex->current_select->item_list.head();
|
||||
DBUG_ASSERT(m_case_expr != nullptr);
|
||||
|
||||
// Return error in release version if m_case_expr == nullptr
|
||||
return m_case_expr == nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
uint m_case_expr_id;
|
||||
Item *m_case_expr;
|
||||
|
@ -63,11 +63,11 @@ const LEX_CSTRING *Sp_rcontext_handler_package_body::get_name_prefix() const
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
sp_rcontext::sp_rcontext(const sp_head *owner,
|
||||
sp_rcontext::sp_rcontext(sp_head *owner,
|
||||
const sp_pcontext *root_parsing_ctx,
|
||||
Field *return_value_fld,
|
||||
bool in_sub_stmt)
|
||||
:end_partial_result_set(false),
|
||||
:callers_arena(nullptr), end_partial_result_set(false),
|
||||
pause_state(false), quit_func(false), instr_ptr(0),
|
||||
m_sp(owner),
|
||||
m_root_parsing_ctx(root_parsing_ctx),
|
||||
@ -91,7 +91,7 @@ sp_rcontext::~sp_rcontext()
|
||||
|
||||
|
||||
sp_rcontext *sp_rcontext::create(THD *thd,
|
||||
const sp_head *owner,
|
||||
sp_head *owner,
|
||||
const sp_pcontext *root_parsing_ctx,
|
||||
Field *return_value_fld,
|
||||
Row_definition_list &field_def_lst)
|
||||
|
@ -71,7 +71,7 @@ public:
|
||||
///
|
||||
/// @return valid sp_rcontext object or NULL in case of OOM-error.
|
||||
static sp_rcontext *create(THD *thd,
|
||||
const sp_head *owner,
|
||||
sp_head *owner,
|
||||
const sp_pcontext *root_parsing_ctx,
|
||||
Field *return_value_fld,
|
||||
Row_definition_list &defs);
|
||||
@ -79,7 +79,7 @@ public:
|
||||
~sp_rcontext();
|
||||
|
||||
private:
|
||||
sp_rcontext(const sp_head *owner,
|
||||
sp_rcontext(sp_head *owner,
|
||||
const sp_pcontext *root_parsing_ctx,
|
||||
Field *return_value_fld,
|
||||
bool in_sub_stmt);
|
||||
@ -169,7 +169,7 @@ public:
|
||||
/// checking if correct runtime context is used for variable handling,
|
||||
/// and to access the package run-time context.
|
||||
/// Also used by slow log.
|
||||
const sp_head *m_sp;
|
||||
sp_head *m_sp;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// SP-variables.
|
||||
|
@ -402,6 +402,15 @@ bool sp_create_assignment_lex(THD *thd, const char *pos)
|
||||
{
|
||||
if (thd->lex->sphead)
|
||||
{
|
||||
if (thd->lex->sphead->is_invoked())
|
||||
/*
|
||||
sphead->is_invoked() is true in case the assignment statement
|
||||
is re-parsed. In this case, a new lex for re-parsing the statement
|
||||
has been already created by sp_lex_instr::parse_expr and it should
|
||||
be used for parsing the assignment SP instruction.
|
||||
*/
|
||||
return false;
|
||||
|
||||
sp_lex_local *new_lex;
|
||||
if (!(new_lex= new (thd->mem_root) sp_lex_set_var(thd, thd->lex)) ||
|
||||
new_lex->main_select_push())
|
||||
@ -436,6 +445,16 @@ bool sp_create_assignment_instr(THD *thd, bool no_lookahead,
|
||||
|
||||
if (lex->sphead)
|
||||
{
|
||||
if (lex->sphead->is_invoked())
|
||||
/*
|
||||
Don't create a new SP assignment instruction in case the current
|
||||
one is re-parsed by reasoning of metadata changes. Since in that case
|
||||
a new lex is also not instantiated (@sa sp_create_assignment_lex)
|
||||
it is safe to just return without restoring old lex that was active
|
||||
before calling SP instruction.
|
||||
*/
|
||||
return false;
|
||||
|
||||
if (!lex->var_list.is_empty())
|
||||
{
|
||||
/*
|
||||
@ -1312,6 +1331,7 @@ void LEX::start(THD *thd_arg)
|
||||
|
||||
table_count_update= 0;
|
||||
|
||||
memset(&trg_chistics, 0, sizeof(trg_chistics));
|
||||
DBUG_VOID_RETURN;
|
||||
}
|
||||
|
||||
@ -3792,14 +3812,20 @@ void st_select_lex::print_limit(THD *thd,
|
||||
void LEX::cleanup_lex_after_parse_error(THD *thd)
|
||||
{
|
||||
/*
|
||||
Delete sphead for the side effect of restoring of the original
|
||||
LEX state, thd->lex, thd->mem_root and thd->free_list if they
|
||||
were replaced when parsing stored procedure statements. We
|
||||
will never use sphead object after a parse error, so it's okay
|
||||
to delete it only for the sake of the side effect.
|
||||
TODO: make this functionality explicit in sp_head class.
|
||||
Sic: we must nullify the member of the main lex, not the
|
||||
current one that will be thrown away
|
||||
Don't delete an instance of the class sp_head pointed by the data member
|
||||
thd->lex->sphead since sp_head's destructor deletes every instruction
|
||||
created during parsing the stored routine. One of deleted instruction
|
||||
is used later in the method sp_head::execute by the following
|
||||
construction
|
||||
ctx->handle_sql_condition(thd, &ip, i)
|
||||
Here the variable 'i' references to the instruction that could be deleted
|
||||
by sp_head's destructor and it would result in server abnormal termination.
|
||||
This use case can theoretically happen in case the current stored routine's
|
||||
instruction causes re-compilation of a SP intruction's statement and
|
||||
internal parse error happens during this process.
|
||||
|
||||
Rather, just restore the original LEX object used before parser has been
|
||||
run.
|
||||
*/
|
||||
if (thd->lex->sphead)
|
||||
{
|
||||
@ -3821,12 +3847,20 @@ void LEX::cleanup_lex_after_parse_error(THD *thd)
|
||||
thd->lex->sphead= NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
sp_head::destroy(thd->lex->sphead);
|
||||
thd->lex->sphead= NULL;
|
||||
}
|
||||
thd->lex->sphead->unwind_aux_lexes_and_restore_original_lex();
|
||||
}
|
||||
else if (thd->lex->sp_mem_root_ptr)
|
||||
{
|
||||
/*
|
||||
A memory root pointed by the data member thd->lex->sp_mem_root_ptr
|
||||
is allocated on compilation of a stored routine. In case the stored
|
||||
routine name is incorrect an instance of the class sp_head hasn't been
|
||||
assigned yet at the moment the error is reported. So, we free here
|
||||
a memory root that allocated for the stored routine having incorrect name.
|
||||
*/
|
||||
free_root(thd->lex->sp_mem_root_ptr, MYF(0));
|
||||
thd->lex->sp_mem_root_ptr= nullptr;
|
||||
}
|
||||
|
||||
/*
|
||||
json_table must be NULL before the query.
|
||||
Didn't want to overload LEX::start, it's enough to put it here.
|
||||
@ -3921,7 +3955,8 @@ LEX::LEX()
|
||||
: explain(NULL), result(0), part_info(NULL), arena_for_set_stmt(0),
|
||||
mem_root_for_set_stmt(0), json_table(NULL), default_used(0),
|
||||
with_rownum(0), is_lex_started(0), option_type(OPT_DEFAULT),
|
||||
context_analysis_only(0), sphead(0), limit_rows_examined_cnt(ULONGLONG_MAX)
|
||||
context_analysis_only(0), sphead(0), sp_mem_root_ptr(nullptr),
|
||||
limit_rows_examined_cnt(ULONGLONG_MAX)
|
||||
{
|
||||
|
||||
init_dynamic_array2(PSI_INSTRUMENT_ME, &plugins, sizeof(plugin_ref),
|
||||
@ -7371,7 +7406,7 @@ sp_head *LEX::make_sp_head(THD *thd, const sp_name *name,
|
||||
sp_head *sp;
|
||||
|
||||
/* Order is important here: new - reset - init */
|
||||
if (likely((sp= sp_head::create(package, sph, agg_type))))
|
||||
if (likely((sp= sp_head::create(package, sph, agg_type, sp_mem_root_ptr))))
|
||||
{
|
||||
sp->reset_thd_mem_root(thd);
|
||||
sp->init(this);
|
||||
@ -10548,6 +10583,24 @@ bool LEX::new_sp_instr_stmt(THD *thd,
|
||||
memcpy(qbuff.str, prefix.str, prefix.length);
|
||||
strmake(qbuff.str + prefix.length, suffix.str, suffix.length);
|
||||
|
||||
/*
|
||||
Force null-termination for every SQL statement inside multi-statements
|
||||
block in order to make the assert
|
||||
|
||||
DBUG_ASSERT(ls->length < UINT_MAX32 &&
|
||||
((ls->length == 0 && !ls->str) ||
|
||||
ls->length == strlen(ls->str)));
|
||||
|
||||
inside the method
|
||||
bool String::append(const LEX_CSTRING *ls)
|
||||
be happy.
|
||||
|
||||
This method is invoked by implementations of the virtual method
|
||||
sp_lex_instr::get_query
|
||||
and overridden implementations of this method in derived classes.
|
||||
*/
|
||||
qbuff.str[prefix.length + suffix.length]= 0;
|
||||
|
||||
if (!(i= new (thd->mem_root) sp_instr_stmt(sphead->instructions(),
|
||||
spcont, this, qbuff)))
|
||||
return true;
|
||||
|
@ -3529,6 +3529,7 @@ public:
|
||||
TABLE_LIST *create_last_non_select_table;
|
||||
sp_head *sphead;
|
||||
sp_name *spname;
|
||||
MEM_ROOT sp_mem_root, *sp_mem_root_ptr;
|
||||
|
||||
sp_pcontext *spcont;
|
||||
|
||||
|
@ -1651,7 +1651,12 @@ bool Table_triggers_list::check_n_load(THD *thd, const LEX_CSTRING *db,
|
||||
|
||||
bool parse_error= parse_sql(thd, & parser_state, creation_ctx);
|
||||
thd->pop_internal_handler();
|
||||
DBUG_ASSERT(!parse_error || lex.sphead == 0);
|
||||
|
||||
if (parse_error)
|
||||
{
|
||||
sp_head::destroy(lex.sphead);
|
||||
lex.sphead= nullptr;
|
||||
}
|
||||
|
||||
/*
|
||||
Not strictly necessary to invoke this method here, since we know
|
||||
@ -1815,6 +1820,7 @@ bool Table_triggers_list::check_n_load(THD *thd, const LEX_CSTRING *db,
|
||||
&trigger->subject_table_grants);
|
||||
}
|
||||
|
||||
sp->m_trg= trigger;
|
||||
lex_end(&lex);
|
||||
}
|
||||
thd->reset_db(&save_db);
|
||||
|
@ -343,6 +343,12 @@ private:
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public:
|
||||
TABLE *get_subject_table()
|
||||
{
|
||||
return trigger_table;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
@ -153,17 +153,17 @@ static Item* escape(THD *thd)
|
||||
|
||||
static void yyerror(THD *thd, const char *s)
|
||||
{
|
||||
/* "parse error" changed into "syntax error" between bison 1.75 and 1.875 */
|
||||
if (strcmp(s,"parse error") == 0 || strcmp(s,"syntax error") == 0)
|
||||
s= ER_THD(thd, ER_SYNTAX_ERROR);
|
||||
thd->parse_error(s, 0);
|
||||
|
||||
/*
|
||||
Restore the original LEX if it was replaced when parsing
|
||||
a stored procedure. We must ensure that a parsing error
|
||||
does not leave any side effects in the THD.
|
||||
*/
|
||||
LEX::cleanup_lex_after_parse_error(thd);
|
||||
|
||||
/* "parse error" changed into "syntax error" between bison 1.75 and 1.875 */
|
||||
if (strcmp(s,"parse error") == 0 || strcmp(s,"syntax error") == 0)
|
||||
s= ER_THD(thd, ER_SYNTAX_ERROR);
|
||||
thd->parse_error(s, 0);
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user