mirror of
https://github.com/MariaDB/server.git
synced 2026-10-23 18:49:29 +03:00
This patch add support of system triggers fired on server start up and shutdown. On start up triggers can be associated only with AFTER event time, on shutdown triggers associated with BEFORE event time. Several triggers can be created to be run on the same time/event type. Core implementation - Extended the sql grammar to support creation of system triggers on startup/shutdown - System triggers metadata is stored in the table mysql.event - Events and system triggers share the same name space since both are stored in the table mysql.event declared with the PRIMARY KEY (`db`,`name`). In result, attempt to create an event with the same name as existent trigger and vice versa results in failure with the new error ER_TRG_EVENT_CONFLICTS_NAME - Since system triggers metadata is stored in the mysql.event table, the table mysql.event is now opened on handling the DROP TRIGGER statement - In case server is started against old version of mysql.event table that missing columns required for support of system triggers, system trigger feature is disabled - System triggers are loaded from the table mysql.event right before their execution - System triggers can be created only by users with the SUPER privilege - Added the option --sys_triggers for mysqldump to dump system triggers. The option is turned off by default. In case this option is on, mysqldump first dump the table 'mysql' unconditionally, regardless whether it was listed in the option --ignore-database
2362 lines
72 KiB
C++
2362 lines
72 KiB
C++
#include "mariadb.h" /* NO_EMBEDDED_ACCESS_CHECKS */
|
|
|
|
#include <m_ctype.h>
|
|
#include <m_string.h>
|
|
#include <mysqld_error.h>
|
|
|
|
#include "table.h"
|
|
|
|
#include "event_common.h" // Event_creation_ctx
|
|
#include "event_db_repository.h" // enum_events_table_field
|
|
#include "event_parse_data.h" // Event_parse_data
|
|
#include "events.h"
|
|
|
|
#include "key.h" // key_copy
|
|
#include "lex_string.h"
|
|
#include "lock.h" // lock_object_name
|
|
#include "log.h" // sql_print_error
|
|
#include "mysqld.h" // next_thread_id
|
|
#include "records.h"
|
|
|
|
#include "scope.h" // scope_exit
|
|
#include "sp_head.h" // sp_head
|
|
#include "sql_base.h" // close_thread_tables
|
|
#include "sql_db.h" // get_default_db_collation
|
|
#include "sql_i_s.h" // schema_table_store_record
|
|
#include "sql_parse.h" // sp_process_definer
|
|
#include "sql_show.h" // append_identifier
|
|
#include "sql_sys_or_ddl_trigger.h"
|
|
#include "sql_table.h" // write_bin_log
|
|
#include "sql_trigger.h"
|
|
#include "strfunc.h" //set_to_string
|
|
#include "wsrep_mysqld.h"
|
|
|
|
static LEX_CSTRING event_table_name{STRING_WITH_LEN("event")};
|
|
|
|
/**
|
|
Raise the error ER_TRG_ALREADY_EXISTS
|
|
*/
|
|
|
|
void report_trg_already_exist_error(const sp_name *spname)
|
|
{
|
|
/*
|
|
Report error in case there is a trigger (either system or on DML event)
|
|
with the same name as the system trigger we are going to create
|
|
*/
|
|
char trigname_buff[FN_REFLEN];
|
|
|
|
strxnmov(trigname_buff, sizeof(trigname_buff) - 1,
|
|
spname->m_db.str, ".",
|
|
spname->m_name.str, NullS);
|
|
my_error(ER_TRG_ALREADY_EXISTS, MYF(0), trigname_buff);
|
|
}
|
|
|
|
|
|
/**
|
|
Check whether there is a trigger with specified name on DML event
|
|
|
|
@return true in case there is a trigger with supplied name on DML event,
|
|
else return false
|
|
*/
|
|
|
|
static bool check_dml_trigger_exist(sp_name *spname)
|
|
{
|
|
char trn_path_buff[FN_REFLEN];
|
|
LEX_CSTRING trn_path= { trn_path_buff, 0 };
|
|
|
|
build_trn_path(spname, (LEX_STRING*) &trn_path);
|
|
|
|
if (!check_trn_exists(&trn_path))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
#define DONT_REPORT_TRG_DOES_NOT_EXIST_ERROR true
|
|
#define REPORT_TRG_DOES_NOT_EXIST_ERROR false
|
|
|
|
/**
|
|
Search for a system or ddl trigger by its name in the table mysql.event
|
|
using index and fetch the record if one there is in the index
|
|
|
|
@param event_table opened mysql.event table
|
|
@param spname trigger name to check
|
|
@param[out] found true in case a record for the trigger name is found,
|
|
else false
|
|
@param[out] has_event_with_the_same_name if point passed in set it to
|
|
the value true in case there event with the same name as
|
|
the trigger
|
|
@param silent does ignore or output the error ER_TRG_DOES_NOT_EXIST
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
static bool fetch_trigger_record_by_name(TABLE *event_table,
|
|
const sp_name *spname,
|
|
bool *found,
|
|
bool *has_event_with_the_same_name,
|
|
bool silent)
|
|
{
|
|
event_table->field[ET_FIELD_DB]->store(spname->m_db.str,
|
|
spname->m_db.length, &my_charset_bin);
|
|
event_table->field[ET_FIELD_NAME]->store(spname->m_name.str,
|
|
spname->m_name.length,
|
|
&my_charset_bin);
|
|
|
|
uchar key[MAX_KEY_LENGTH];
|
|
key_copy(key, event_table->record[0], event_table->key_info,
|
|
event_table->key_info->key_length);
|
|
|
|
int ret= event_table->file->ha_index_read_idx_map(event_table->record[0], 0,
|
|
key, HA_WHOLE_KEY,
|
|
HA_READ_KEY_EXACT);
|
|
/*
|
|
ret != 0 in case error or 'row not found' condition;
|
|
ret == 0 if 'row found'
|
|
*/
|
|
if (!ret)
|
|
{
|
|
if (is_sys_trigger_row(event_table))
|
|
*found= true;
|
|
else
|
|
{
|
|
*found= false;
|
|
|
|
if (has_event_with_the_same_name)
|
|
{
|
|
*has_event_with_the_same_name= true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
Trigger doesn't exist in case the record is for the real event
|
|
*/
|
|
if (!silent)
|
|
{
|
|
my_error(ER_TRG_DOES_NOT_EXIST, MYF(0));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (ret == HA_ERR_KEY_NOT_FOUND || ret == HA_ERR_END_OF_FILE)
|
|
{
|
|
*found= false;
|
|
if (!silent)
|
|
{
|
|
my_error(ER_TRG_DOES_NOT_EXIST, MYF(0));
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
event_table->file->print_error(ret, MYF(0));
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static inline bool report_data_too_long_error(Field *field)
|
|
{
|
|
my_error(ER_EVENT_DATA_TOO_LONG, MYF(0),
|
|
field->field_name.str);
|
|
return true;
|
|
}
|
|
|
|
static inline bool report_event_store_failed_error(Field *field, int ret)
|
|
{
|
|
my_error(ER_EVENT_STORE_FAILED, MYF(0),
|
|
field->field_name.str, ret);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
Store information about the trigger being created into the table mysql.event
|
|
|
|
@param thd Thread handler
|
|
@param lex LEX used for parsing the original CREATE TRIGGER statement
|
|
@param event_table Opened table mysql.event where to store the trigger's
|
|
metadata
|
|
@param sphead an instance of sp_head created for trigger
|
|
@param trg_chistics trigger characteristics (event time, event kind, etc)
|
|
@pram sql_mode sql_mode used for creation of the trigger
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
static bool store_trigger_metadata(THD *thd, LEX *lex, TABLE *event_table,
|
|
sp_head *sphead,
|
|
const st_trg_chistics &trg_chistics,
|
|
sql_mode_t sql_mode)
|
|
{
|
|
restore_record(event_table, s->default_values);
|
|
|
|
if (sphead->m_body.length > event_table->field[ET_FIELD_BODY]->field_length)
|
|
{
|
|
my_error(ER_TOO_LONG_BODY, MYF(0), sphead->m_name.str);
|
|
|
|
return true;
|
|
}
|
|
|
|
Field **fields= event_table->field;
|
|
int ret;
|
|
|
|
char definer_buf[USER_HOST_BUFF_SIZE];
|
|
LEX_CSTRING definer;
|
|
lex->definer->set_lex_string(&definer, definer_buf);
|
|
|
|
if (fields[ET_FIELD_DEFINER]->store(definer.str, definer.length,
|
|
system_charset_info))
|
|
return report_data_too_long_error(fields[ET_FIELD_DEFINER]);
|
|
|
|
if (fields[ET_FIELD_DB]->store(sphead->m_db.str,
|
|
sphead->m_db.length,
|
|
system_charset_info))
|
|
return report_data_too_long_error(fields[ET_FIELD_DB]);
|
|
|
|
if (fields[ET_FIELD_NAME]->store(sphead->m_name.str,
|
|
sphead->m_name.length,
|
|
system_charset_info))
|
|
return report_data_too_long_error(fields[ET_FIELD_NAME]);
|
|
|
|
fields[ET_FIELD_CHARACTER_SET_CLIENT]->set_notnull();
|
|
ret= fields[ET_FIELD_CHARACTER_SET_CLIENT]->store(
|
|
&thd->variables.character_set_client->cs_name,
|
|
system_charset_info);
|
|
if (ret)
|
|
return
|
|
report_event_store_failed_error(fields[ET_FIELD_CHARACTER_SET_CLIENT],
|
|
ret);
|
|
|
|
fields[ET_FIELD_COLLATION_CONNECTION]->set_notnull();
|
|
ret= fields[ET_FIELD_COLLATION_CONNECTION]->
|
|
store(&thd->variables.collation_connection->coll_name,
|
|
system_charset_info);
|
|
if (ret)
|
|
return
|
|
report_event_store_failed_error(fields[ET_FIELD_COLLATION_CONNECTION],
|
|
ret);
|
|
|
|
CHARSET_INFO *db_cl= get_default_db_collation(thd, sphead->m_db.str);
|
|
|
|
fields[ET_FIELD_DB_COLLATION]->set_notnull();
|
|
ret= fields[ET_FIELD_DB_COLLATION]->store(&db_cl->coll_name,
|
|
system_charset_info);
|
|
if (ret)
|
|
return report_event_store_failed_error(fields[ET_FIELD_DB_COLLATION],
|
|
ret);
|
|
|
|
ret= fields[ET_FIELD_ON_COMPLETION]->store(
|
|
(longlong)Event_parse_data::ON_COMPLETION_PRESERVE, true);
|
|
if (ret)
|
|
return report_event_store_failed_error(fields[ET_FIELD_ON_COMPLETION],
|
|
ret);
|
|
|
|
ret= fields[ET_FIELD_ORIGINATOR]->store(
|
|
(longlong)global_system_variables.server_id, true);
|
|
if (ret)
|
|
return report_event_store_failed_error(fields[ET_FIELD_ORIGINATOR],
|
|
ret);
|
|
|
|
ret= fields[ET_FIELD_CREATED]->set_time();
|
|
if (ret)
|
|
return report_event_store_failed_error(fields[ET_FIELD_CREATED],
|
|
ret);
|
|
|
|
ret= fields[ET_FIELD_SQL_MODE]->store((longlong)sql_mode, true);
|
|
if (ret)
|
|
return report_event_store_failed_error(fields[ET_FIELD_SQL_MODE],
|
|
ret);
|
|
|
|
ret= fields[ET_FIELD_BODY]->store(sphead->m_body.str,
|
|
sphead->m_body.length,
|
|
system_charset_info);
|
|
if (ret)
|
|
return report_event_store_failed_error(fields[ET_FIELD_BODY],
|
|
ret);
|
|
|
|
/*
|
|
trg_chistics.events has meaningful bits for every trigger events,
|
|
that is for DML, DDL, system events. Matching of event types and values of
|
|
trg_chistics.events is depicted below:
|
|
ON INSERT = 0x01
|
|
ON UPDATE = 0x02
|
|
ON DELETE = 0x04
|
|
ON STARTUP = 0x08
|
|
ON SHUTDOWN =0x10
|
|
ON LOGON = 0x20
|
|
ON LOGOFF = 0x40
|
|
ON DDL = 0x80
|
|
|
|
The table mysql.event declares the column `kind` as a set with
|
|
the following values
|
|
`kind` set('SCHEDULE','STARTUP','SHUTDOWN','LOGON','LOGOFF','DDL')
|
|
|
|
So, events for system triggers stored in the column mysql.event.kind
|
|
has the following values:
|
|
SCHEDULE = 0x01
|
|
STARTUP = 0x02
|
|
SHUTDOWN = 0x04
|
|
LOGON = 0x08
|
|
LOGOFF = 0x10
|
|
DDL = 0x20
|
|
|
|
So, for mapping trg_chistics.events to mysql.event.kind we have to
|
|
shift trg_chistics.events to the right by 3 bits (to get the first not DML
|
|
trigger event type) and then shift to the left by 1 bit to take into
|
|
account the enumerator value for SCHENDULE
|
|
*/
|
|
longlong trg_events= (trg_chistics.events >> 3);
|
|
ret= fields[ET_FIELD_KIND]->store((trg_events << 1), true);
|
|
if (ret)
|
|
return report_event_store_failed_error(fields[ET_FIELD_KIND],
|
|
ret);
|
|
|
|
ret= fields[ET_FIELD_WHEN]->store((longlong)trg_chistics.action_time + 1,
|
|
true);
|
|
if (ret)
|
|
return report_event_store_failed_error(fields[ET_FIELD_WHEN],
|
|
ret);
|
|
fields[ET_FIELD_WHEN]->set_notnull();
|
|
|
|
ret= event_table->file->ha_write_row(event_table->record[0]);
|
|
if (ret)
|
|
{
|
|
event_table->file->print_error(ret, MYF(0));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
The class Transaction_Resources_Guard is RAII class to recover
|
|
the transaction related state as it was before entering constructor.
|
|
It is typically used for restoring sql_mode and binlog_format on any return
|
|
path from a function where this guard class is instantiated.
|
|
*/
|
|
|
|
class Transaction_Resources_Guard : public Sql_mode_instant_set
|
|
{
|
|
public:
|
|
explicit Transaction_Resources_Guard(THD *thd)
|
|
: Sql_mode_instant_set(thd, 0),
|
|
m_saved_binlog_format{BINLOG_FORMAT_UNSPEC},
|
|
m_restore_binlog_format{false}
|
|
{}
|
|
Transaction_Resources_Guard(THD *thd,
|
|
enum_binlog_format saved_binlog_format)
|
|
: Sql_mode_instant_set(thd, 0),
|
|
m_saved_binlog_format{saved_binlog_format},
|
|
m_restore_binlog_format{true}
|
|
{}
|
|
~Transaction_Resources_Guard()
|
|
{
|
|
if (m_restore_binlog_format)
|
|
thd->restore_stmt_binlog_format(m_saved_binlog_format);
|
|
}
|
|
private:
|
|
enum_binlog_format m_saved_binlog_format;
|
|
bool m_restore_binlog_format;
|
|
};
|
|
|
|
/**
|
|
Per process THD. Used on execution of ON STARTUP/ON SHUTDOWN system triggers
|
|
to run statements on behalf the whole server process.
|
|
*/
|
|
class THD_PROCESS_WIDE : public THD
|
|
{
|
|
public:
|
|
explicit THD_PROCESS_WIDE(my_thread_id id) : THD(id, false, true)
|
|
{}
|
|
};
|
|
|
|
static THD_PROCESS_WIDE *thd_for_sys_triggers= nullptr;
|
|
|
|
/**
|
|
Final steps executed on successful run CREATE TRIGGER statement
|
|
|
|
@param thd Thread handler
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
inline static bool finalize_stmt(THD *thd)
|
|
{
|
|
char buffer[1024];
|
|
String log_query(buffer, sizeof(buffer), &my_charset_bin);
|
|
|
|
if (create_query_string(thd, &log_query) ||
|
|
write_bin_log(thd, true, log_query.ptr(), log_query.length()))
|
|
return true;
|
|
|
|
my_ok(thd);
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
Final steps executed on name conflict and presence of 'IF NOT EXIST' clause
|
|
|
|
@param thd Thread handler
|
|
@param trg_name Trigger name
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
inline static bool finalize_stmt(THD *thd, const char *trg_name)
|
|
{
|
|
push_warning_printf(thd, Sql_condition::WARN_LEVEL_NOTE,
|
|
ER_TRG_ALREADY_EXISTS,
|
|
ER_THD(thd, ER_TRG_ALREADY_EXISTS),
|
|
trg_name);
|
|
|
|
return finalize_stmt(thd);
|
|
}
|
|
|
|
|
|
static bool mysql_create_sys_trigger_impl(THD *thd)
|
|
{
|
|
DBUG_ASSERT(thd->lex->spname->m_db.length != 0);
|
|
|
|
if (check_db_dir_existence(thd->lex->spname->m_db.str))
|
|
{
|
|
my_error(ER_BAD_DB_ERROR, MYF(0), thd->lex->spname->m_db.str);
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
We don't allow creating system triggers in the 'mysql' schema
|
|
*/
|
|
if (thd->lex->spname->m_db.streq(MYSQL_SCHEMA_NAME))
|
|
{
|
|
my_error(ER_NO_TRIGGERS_ON_SYSTEM_SCHEMA, MYF(0));
|
|
return true;
|
|
}
|
|
|
|
if (thd->lex->trg_chistics.action_time == TRG_ACTION_BEFORE &&
|
|
(sys_trg2bit(TRG_EVENT_STARTUP) & thd->lex->trg_chistics.events))
|
|
{
|
|
my_error(ER_SYS_TRG_SEMANTIC_ERROR, MYF(0), thd->lex->spname->m_db.str,
|
|
thd->lex->spname->m_name.str, "BEFORE", "STARTUP");
|
|
return true;
|
|
}
|
|
|
|
if (thd->lex->trg_chistics.action_time == TRG_ACTION_AFTER &&
|
|
(sys_trg2bit(TRG_EVENT_SHUTDOWN) & thd->lex->trg_chistics.events))
|
|
{
|
|
my_error(ER_SYS_TRG_SEMANTIC_ERROR, MYF(0), thd->lex->spname->m_db.str,
|
|
thd->lex->spname->m_name.str, "AFTER", "SHUTDOWN");
|
|
return true;
|
|
}
|
|
|
|
if (sp_process_definer(thd))
|
|
return true;
|
|
|
|
sql_mode_t saved_mode= thd->variables.sql_mode;
|
|
enum_binlog_format binlog_fmt= thd->set_current_stmt_binlog_format_stmt();
|
|
/*
|
|
Reset sql_mode during data dictionary operations and activate the guard
|
|
to restore original sql_mode and binlog format on any return path from
|
|
this function.
|
|
*/
|
|
Transaction_Resources_Guard transaction_guard{thd, binlog_fmt};
|
|
|
|
/*
|
|
Use the MDL_key::TRIGGER namespace for acquiring the mdl lock on system
|
|
trigger name to guard against attempt to create a DML trigger with the same
|
|
name as the system trigger being created.
|
|
*/
|
|
if (lock_object_name(thd, MDL_key::TRIGGER, thd->lex->spname->m_db,
|
|
thd->lex->spname->m_name))
|
|
return true;
|
|
|
|
if (check_dml_trigger_exist(thd->lex->spname))
|
|
{
|
|
if (thd->lex->create_info.if_not_exists())
|
|
return finalize_stmt(thd, thd->lex->spname->m_name.str);
|
|
|
|
/*
|
|
Report error in case there is a trigger on DML event with
|
|
the same name as the system trigger we are going to create
|
|
*/
|
|
report_trg_already_exist_error(thd->lex->spname);
|
|
return true;
|
|
}
|
|
|
|
TABLE *event_table;
|
|
bool sys_triggers_enabled;
|
|
if (Event_db_repository::open_event_table(thd, TL_WRITE, &event_table,
|
|
&sys_triggers_enabled))
|
|
return true;
|
|
|
|
if (!sys_triggers_enabled)
|
|
{
|
|
my_error(ER_SYSTEM_TRG_DISABLED, MYF(0));
|
|
return true;
|
|
}
|
|
|
|
|
|
bool has_event_with_the_same_name= false, found;
|
|
if (fetch_trigger_record_by_name(event_table, thd->lex->spname,
|
|
&found,
|
|
&has_event_with_the_same_name,
|
|
DONT_REPORT_TRG_DOES_NOT_EXIST_ERROR))
|
|
return true;
|
|
|
|
if (has_event_with_the_same_name)
|
|
{
|
|
my_error(ER_TRG_EVENT_CONFLICTS_NAME, MYF(0),
|
|
"Event", thd->lex->spname->m_name.str);
|
|
return true;
|
|
}
|
|
|
|
if (found)
|
|
{
|
|
if (thd->lex->create_info.if_not_exists())
|
|
return finalize_stmt(thd, thd->lex->spname->m_name.str);
|
|
|
|
if (thd->lex->create_info.or_replace())
|
|
{
|
|
/* Drop the existing trigger */
|
|
int ret= event_table->file->ha_delete_row(event_table->record[0]);
|
|
if (ret)
|
|
{
|
|
event_table->file->print_error(ret, MYF(0));
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
report_trg_already_exist_error(thd->lex->spname);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (store_trigger_metadata(thd, thd->lex, event_table, thd->lex->sphead,
|
|
thd->lex->trg_chistics, saved_mode))
|
|
return true;
|
|
|
|
return finalize_stmt(thd);
|
|
}
|
|
|
|
|
|
/**
|
|
Handle the statement CREATE TRIGGER for system triggers,
|
|
such as ON STARTUP, ON SHUTDOWN.
|
|
|
|
@param thd Thread handler
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
bool mysql_create_sys_trigger(THD *thd)
|
|
{
|
|
/*
|
|
The SUPER privilege is required to create ON STARTUP/ON SHUTDOWN
|
|
system triggers
|
|
*/
|
|
if (check_global_access(thd, SUPER_ACL))
|
|
return true;
|
|
|
|
WSREP_TO_ISOLATION_BEGIN(WSREP_MYSQL_DB, NULL, NULL);
|
|
|
|
return mysql_create_sys_trigger_impl(thd);
|
|
|
|
#ifdef WITH_WSREP
|
|
wsrep_error_label:
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
|
|
/**
|
|
Check whether support of system triggers enabled and there is a trigger
|
|
with specified name. Do it in a separate transaction to work under
|
|
LOCK TABLE.
|
|
|
|
@param thd Thread handler
|
|
@param spname the trigger name to check for existence
|
|
@param[out] found On return from this function has the value true in case
|
|
the trigger found by name, else false
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
bool find_sys_trigger_by_name(THD *thd, sp_name *spname, bool *found)
|
|
{
|
|
start_new_trans new_trans(thd);
|
|
TABLE_LIST event_table;
|
|
|
|
event_table.init_one_table(&MYSQL_SCHEMA_NAME,
|
|
&event_table_name, 0, TL_READ);
|
|
|
|
if (open_system_tables_for_read(thd, &event_table))
|
|
{
|
|
if (thd->get_stmt_da()->get_sql_errno() == ER_NO_SUCH_TABLE)
|
|
{
|
|
/*
|
|
Missing the table mysql.event or the whole database mysql
|
|
shouldn't be treated as error because of backward compatibility -
|
|
previous versions of mysql/mariadb used to rely entirely on
|
|
.TRG/.TRN files while checking for trigger for presence. So,
|
|
missing the mysql.event should be treated as 'no system trigger found'
|
|
and fall back to handling a regular DML trigger.
|
|
*/
|
|
*found= false;
|
|
thd->clear_error();
|
|
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
auto scope_cleaner = make_scope_exit(
|
|
[&thd]()
|
|
{
|
|
thd->commit_whole_transaction_and_close_tables();
|
|
}
|
|
);
|
|
|
|
bool system_triggers_enabled;
|
|
if (Event_db_repository_common::check_event_table_for_validness(
|
|
event_table.table,
|
|
&system_triggers_enabled))
|
|
{
|
|
my_error(ER_EVENT_OPEN_TABLE_FAILED, MYF(0));
|
|
return true;
|
|
}
|
|
|
|
if (!system_triggers_enabled)
|
|
{
|
|
*found= false;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ret= fetch_trigger_record_by_name(event_table.table, spname,
|
|
found,
|
|
nullptr,
|
|
DONT_REPORT_TRG_DOES_NOT_EXIST_ERROR);
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
/**
|
|
Handle the statement DROP TRIGGER for system triggers,
|
|
such as ON STARTUP, ON SHUTDOWN. Trigger name is specified by
|
|
thd->lex->spname.
|
|
|
|
@param thd Thread handler
|
|
@param[out] no_ddl_trigger_found true in case there is no trigger with
|
|
the specified name, else false
|
|
|
|
@return false on success, true on error
|
|
@note the case `trigger not found` is considered as a success result
|
|
with setting the @param no_ddl_trigger_found to the true value
|
|
*/
|
|
|
|
bool mysql_drop_sys_or_ddl_trigger(THD *thd,
|
|
bool *no_ddl_trigger_found)
|
|
{
|
|
if (!thd->lex->spname->m_db.length)
|
|
{
|
|
my_error(ER_NO_DB_ERROR, MYF(0));
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
First check whether support of system triggers enabled and then check
|
|
whether the trigger does exist. It is performed by a separate function
|
|
that opens the table mysql.event on reading within a new independent
|
|
transaction to handle the case when DROP TRIGGER be executed in
|
|
locked_tables_mode and there is no a trigger with the supplied name.
|
|
*/
|
|
bool found= false;
|
|
|
|
if (find_sys_trigger_by_name(thd, thd->lex->spname, &found))
|
|
return true;
|
|
|
|
if (!found)
|
|
{
|
|
/*
|
|
The use case 'trigger not found' is handled at the function
|
|
mysql_create_or_drop_trigger() if there is no a DML trigger
|
|
with specified name
|
|
*/
|
|
*no_ddl_trigger_found= true;
|
|
|
|
return false;
|
|
}
|
|
|
|
*no_ddl_trigger_found= false;
|
|
|
|
enum_binlog_format binlog_fmt= thd->set_current_stmt_binlog_format_stmt();
|
|
/*
|
|
Reset sql_mode during data dictionary operations and activate the guard
|
|
to restore original sql_mode and binlog format on any return path from
|
|
this function.
|
|
*/
|
|
Transaction_Resources_Guard transaction_guard{thd, binlog_fmt};
|
|
|
|
TABLE *event_table;
|
|
bool sys_triggers_enabled;
|
|
int ret;
|
|
|
|
WSREP_TO_ISOLATION_BEGIN(WSREP_MYSQL_DB, NULL, NULL);
|
|
|
|
/* Protect against concurrent create/drop */
|
|
if (lock_object_name(thd, MDL_key::TRIGGER, thd->lex->spname->m_db,
|
|
thd->lex->spname->m_name))
|
|
return true;
|
|
|
|
/*
|
|
The SUPER privilege is required to drop ON STARTUP/ON SHUTDOWN
|
|
system triggers. Check the privilege after ensuring the system trigger
|
|
does really exist since the function mysql_drop_sys_or_ddl_trigger is also
|
|
called for DML triggers.
|
|
*/
|
|
if (check_global_access(thd, SUPER_ACL))
|
|
return true;
|
|
|
|
if (Event_db_repository::open_event_table(thd, TL_WRITE, &event_table,
|
|
&sys_triggers_enabled))
|
|
return true;
|
|
|
|
/*
|
|
Check existence of a trigger by its name the second time.
|
|
Do that this time under protection of MDL lock in the namespace
|
|
MDL_key::TRIGGER
|
|
It is not possible to check for presence of a trigger by its name
|
|
immediately under the MDL lock since that would result in broken
|
|
some tests
|
|
*/
|
|
ret= fetch_trigger_record_by_name(event_table, thd->lex->spname,
|
|
&found,
|
|
nullptr,
|
|
DONT_REPORT_TRG_DOES_NOT_EXIST_ERROR);
|
|
if (ret)
|
|
return true;
|
|
|
|
if (!found)
|
|
{
|
|
*no_ddl_trigger_found= true;
|
|
return false;
|
|
}
|
|
|
|
ret= event_table->file->ha_delete_row(event_table->record[0]);
|
|
if (ret)
|
|
event_table->file->print_error(ret, MYF(0));
|
|
else
|
|
{
|
|
/* Binlog the drop trigger statement. */
|
|
if (write_bin_log(thd, false, thd->query(), thd->query_length()))
|
|
return true;
|
|
|
|
my_ok(thd);
|
|
}
|
|
|
|
return ret;
|
|
|
|
#ifdef WITH_WSREP
|
|
wsrep_error_label:
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
bool Sys_trigger::execute()
|
|
{
|
|
List<Item> empty_item_list;
|
|
|
|
m_thd->reset_for_next_command();
|
|
|
|
#ifndef NO_EMBEDDED_ACCESS_CHECKS
|
|
Security_context trg_sctx, *save_sctx= NULL;
|
|
|
|
if (trg_sctx.change_security_context(m_thd,
|
|
&m_sp->m_definer.user,
|
|
&m_sp->m_definer.host,
|
|
&m_sp->m_db, &save_sctx))
|
|
{
|
|
if (save_sctx)
|
|
trg_sctx.restore_security_context(m_thd, save_sctx);
|
|
|
|
sql_print_error("System trigger %s execution failing, "
|
|
"failed to authenticate the user %s@%s.",
|
|
m_sp->m_qname.str, m_sp->m_definer.user.str,
|
|
m_sp->m_definer.host.str);
|
|
|
|
return true;
|
|
}
|
|
if (mysql_change_db(m_thd, m_sp->m_db, false))
|
|
{
|
|
if (save_sctx)
|
|
trg_sctx.restore_security_context(m_thd, save_sctx);
|
|
|
|
sql_print_error("System trigger %s execution failing, "
|
|
"failed to switch to the db %s.",
|
|
m_sp->m_qname.str, m_sp->m_db.str);
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
bool ret= m_sp->execute_procedure(m_thd, &empty_item_list);
|
|
|
|
if (ret)
|
|
sql_print_error("System trigger (%s) execution failing %d (%s)",
|
|
m_sp->m_qname.str,
|
|
m_thd->get_stmt_da()->sql_errno(),
|
|
m_thd->get_stmt_da()->message());
|
|
#ifndef NO_EMBEDDED_ACCESS_CHECKS
|
|
if (save_sctx)
|
|
trg_sctx.restore_security_context(m_thd, save_sctx);
|
|
#endif
|
|
|
|
close_thread_tables_for_query(m_thd);
|
|
|
|
m_thd->lex->unit.cleanup();
|
|
m_thd->end_statement();
|
|
m_thd->cleanup_after_query();
|
|
m_thd->reset_query();
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
static constexpr LEX_CSTRING base_event_time[]= {
|
|
LEX_CSTRING{STRING_WITH_LEN("BEFORE")},
|
|
LEX_CSTRING{STRING_WITH_LEN("AFTER")}
|
|
};
|
|
|
|
static constexpr LEX_CSTRING base_event_names[]= {
|
|
LEX_CSTRING{STRING_WITH_LEN("SCHEDULE")},
|
|
LEX_CSTRING{STRING_WITH_LEN("STARTUP")},
|
|
LEX_CSTRING{STRING_WITH_LEN("SHUTDOWN")},
|
|
LEX_CSTRING{STRING_WITH_LEN("LOGON")},
|
|
LEX_CSTRING{STRING_WITH_LEN("LOGOFF")},
|
|
LEX_CSTRING{STRING_WITH_LEN("DDL")}
|
|
};
|
|
|
|
static constexpr size_t max_event_names_length =
|
|
(base_event_names[0].length + 1) +
|
|
(base_event_names[1].length + 1) +
|
|
(base_event_names[2].length + 1) +
|
|
(base_event_names[3].length + 1) +
|
|
(base_event_names[4].length + 1) +
|
|
(base_event_names[5].length + 1);
|
|
|
|
|
|
/**
|
|
Convert the value of trigger kind mask into a comma separated strings value
|
|
|
|
@param base_event_names - array of names for every event type retrieved
|
|
from the column mysq.event.kind. These names are
|
|
in the following order:
|
|
base_event_names[0] == "SCHEDULE"
|
|
base_event_names[1] == "STARTUP"
|
|
base_event_names[2] == "SHUTDOWN"
|
|
base_event_names[3] == "LOGON",
|
|
base_event_names[4] == "LOGOFF"
|
|
base_event_names[5] == "DDL"
|
|
@param [out] set_of_events - text representation of trigger events bitmap
|
|
value
|
|
@param trg_kind - bitmap containing turned on bit for every event the system
|
|
trigger is created for. Events mask numbering is started
|
|
from the bit number 1, bit number 0 is reserved for the
|
|
special value 'SCHEDULE'. The parameter trg_kind never
|
|
contains a value with the 0 bit set.
|
|
|
|
@return LEX_CSTRING containing text representation of trigger events bitmap
|
|
value
|
|
*/
|
|
|
|
static LEX_CSTRING events_to_string(const LEX_CSTRING base_event_names[],
|
|
char *set_of_events,
|
|
const Event_parse_data::enum_kind trg_kind)
|
|
{
|
|
size_t offset= 0;
|
|
|
|
/*
|
|
Empty result C-string to get a printable result string
|
|
in case trg_kind equals 0
|
|
*/
|
|
set_of_events[offset]= 0;
|
|
/*
|
|
Shift right by one bit since the bit for "SCHEDULE" is never set in
|
|
the argument trg_kind
|
|
*/
|
|
ulonglong kind= ((ulonglong )trg_kind) >> 1;
|
|
for (int idx= 1; kind != 0; kind= kind >> 1, idx++)
|
|
{
|
|
if (kind & 0x1)
|
|
offset+= snprintf(set_of_events + offset,
|
|
max_event_names_length - offset, "%s,",
|
|
base_event_names[idx].str);
|
|
}
|
|
if (offset)
|
|
{
|
|
set_of_events[offset - 1]= 0;
|
|
offset= offset - 1;
|
|
}
|
|
return LEX_CSTRING{set_of_events, offset};
|
|
}
|
|
|
|
|
|
/**
|
|
Based on input parameter values, assemble the CREATE TRIGGER statement
|
|
used to create the system trigger.
|
|
|
|
@param thd Thread handler
|
|
@param[out] create_trg_stmt Where to store the resulted
|
|
CREATE TRIGGER statement
|
|
@param trg_definer Definer used for trigger creation
|
|
@param trg_name Trigger name
|
|
@param trg_kind Trigger kind (ON STARTUP, ON SHUTDOWN, etc)
|
|
@param trg_when Trigger event time (BEFORE, AFTER)
|
|
@param body Trigger body
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
static bool reconstruct_create_trigger_stmt(
|
|
THD *thd, String *create_trg_stmt,
|
|
const LEX_CSTRING &trg_definer,
|
|
const LEX_CSTRING &trg_name,
|
|
Event_parse_data::enum_kind trg_kind,
|
|
trg_action_time_type trg_when,
|
|
const LEX_STRING &body)
|
|
{
|
|
static const LEX_CSTRING prefix{STRING_WITH_LEN("CREATE DEFINER=")};
|
|
|
|
static const LEX_CSTRING trigger_clause{STRING_WITH_LEN(" TRIGGER ")};
|
|
|
|
char *buffer;
|
|
size_t buffer_len= prefix.length + trg_definer.length +
|
|
trigger_clause.length + trg_name.length + 1 +
|
|
base_event_time[trg_when].length + 1 +
|
|
max_event_names_length + 1 +
|
|
body.length + 1;
|
|
buffer= thd->alloc(buffer_len);
|
|
if (buffer == nullptr)
|
|
return true;
|
|
|
|
create_trg_stmt->set(buffer, buffer_len, system_charset_info);
|
|
create_trg_stmt->length(0);
|
|
|
|
char event_names_buf[max_event_names_length + 1];
|
|
bool ret= create_trg_stmt->append(STRING_WITH_LEN("CREATE ")) ||
|
|
create_trg_stmt->append_name_value(
|
|
LEX_CSTRING{STRING_WITH_LEN("DEFINER")},
|
|
trg_definer) ||
|
|
create_trg_stmt->append(trigger_clause) ||
|
|
append_identifier(thd, create_trg_stmt, &trg_name) ||
|
|
create_trg_stmt->append(' ') ||
|
|
create_trg_stmt->append(base_event_time[trg_when]) ||
|
|
create_trg_stmt->append(' ') ||
|
|
|
|
create_trg_stmt->append(events_to_string(base_event_names,
|
|
event_names_buf,
|
|
trg_kind)) ||
|
|
create_trg_stmt->append(' ') ||
|
|
create_trg_stmt->append(body, system_charset_info);
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
/**
|
|
Construct the CREATE PROCEDURE statememt that is passed to parser.
|
|
The statement CREATE PROCEDURE is contructed instead of CREATE TRIGGER
|
|
since system triggers are considedr as stored procedures.
|
|
The CREATE PROCEDURE statement is created with SQL SECURITY INVOKER clause
|
|
|
|
@param thd thread context
|
|
@param[out] create_trg_stmt buffer where to write the resulting statement
|
|
@param trg_name trigger name
|
|
@param body trigger body
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
static bool construct_sp_sql_for_sys_trigger(
|
|
THD *thd, String *create_trg_stmt,
|
|
const LEX_CSTRING &trg_name,
|
|
const LEX_CSTRING &body)
|
|
{
|
|
static const LEX_CSTRING prefix{STRING_WITH_LEN("CREATE PROCEDURE ")};
|
|
static const LEX_CSTRING security_invoker_str{
|
|
STRING_WITH_LEN("() SQL SECURITY INVOKER ")};
|
|
static const LEX_CSTRING oracle_begin_str{STRING_WITH_LEN(" AS BEGIN ")};
|
|
static const LEX_CSTRING oracle_end_str{STRING_WITH_LEN("; END")};
|
|
size_t buffer_len= prefix.length +
|
|
security_invoker_str.length +
|
|
trg_name.length + 1 +
|
|
body.length + 1 +
|
|
((thd->variables.sql_mode & MODE_ORACLE) ?
|
|
oracle_begin_str.length + oracle_end_str.length : 0);
|
|
char *buffer= thd->alloc(buffer_len);
|
|
|
|
if (buffer == nullptr)
|
|
return true;
|
|
|
|
create_trg_stmt->set(buffer, buffer_len, system_charset_info);
|
|
create_trg_stmt->length(0);
|
|
|
|
bool ret=
|
|
create_trg_stmt->append(prefix) ||
|
|
append_identifier(thd, create_trg_stmt, &trg_name) ||
|
|
create_trg_stmt->append(security_invoker_str) ||
|
|
create_trg_stmt->append((thd->variables.sql_mode & MODE_ORACLE) ?
|
|
oracle_begin_str : empty_clex_str) ||
|
|
create_trg_stmt->append(&body) ||
|
|
create_trg_stmt->append((thd->variables.sql_mode & MODE_ORACLE) ?
|
|
oracle_end_str : empty_clex_str);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
RAII class to restore original lex object on return from the function
|
|
compile_trigger_stmt().
|
|
*/
|
|
|
|
class Trigger_Compilation_Resources_Guard
|
|
{
|
|
public:
|
|
explicit Trigger_Compilation_Resources_Guard(THD *thd)
|
|
: m_thd{thd}, m_lex{thd->lex}
|
|
{}
|
|
~Trigger_Compilation_Resources_Guard()
|
|
{
|
|
m_thd->lex= m_lex;
|
|
}
|
|
private:
|
|
THD *m_thd;
|
|
LEX *m_lex;
|
|
};
|
|
|
|
|
|
/**
|
|
Parse the CREATE PROCEDURE statement reconstructed from the original
|
|
CREATE TRIGGER statement and return sp_head for compiled kinda trigger.
|
|
|
|
@param thd Thread context
|
|
@param db_name database name
|
|
@param create_trigger_stmt reconstructed from trigger metadata
|
|
the CREATE PROCEDURE statement to compile
|
|
@param ctx Trigger creation context
|
|
@param[out] parse_error output parameter for storing result of
|
|
parsing the statement: false on success,
|
|
true on error
|
|
|
|
@return sp_head object on success, nullptr on error
|
|
*/
|
|
|
|
static sp_head *compile_trigger_stmt(THD *thd,
|
|
const LEX_CSTRING &db_name,
|
|
const String *create_trigger_stmt,
|
|
Stored_program_creation_ctx *ctx,
|
|
bool *parse_error)
|
|
{
|
|
LEX lex;
|
|
Parser_state parser_state;
|
|
|
|
Trigger_Compilation_Resources_Guard guard{thd};
|
|
thd->set_db(&db_name);
|
|
thd->lex= &lex;
|
|
|
|
if (parser_state.init(thd, (char*) create_trigger_stmt->ptr(),
|
|
create_trigger_stmt->length()))
|
|
return nullptr;
|
|
|
|
lex_start(thd);
|
|
/*
|
|
Nullify lex->sphead explicitly in order to be sure that on return
|
|
from parse_sql() it either contains a valid pointer on allocated
|
|
instance of sp_head or null pointer and never contains garbage value.
|
|
*/
|
|
thd->lex->sphead= nullptr;
|
|
thd->spcont= NULL;
|
|
lex.trg_chistics.events= TRG_EVENT_UNKNOWN;
|
|
lex.trg_chistics.action_time= TRG_ACTION_MAX;
|
|
|
|
*parse_error= parse_sql(thd, &parser_state, ctx);
|
|
|
|
if (*parse_error)
|
|
{
|
|
/*
|
|
Parse error could happen after sp_head has been created
|
|
so free it explicitly to avoid memory leaks
|
|
*/
|
|
lex_end(&lex);
|
|
sp_head::destroy(thd->lex->sphead);
|
|
thd->lex->sphead= nullptr;
|
|
return nullptr;
|
|
}
|
|
|
|
sp_head *sphead= thd->lex->sphead;
|
|
if (sphead != nullptr)
|
|
sphead->init_psi_share();
|
|
|
|
thd->lex->sphead= nullptr;
|
|
|
|
lex_end(&lex);
|
|
|
|
return sphead;
|
|
}
|
|
|
|
|
|
/**
|
|
Based on trigger's meta-data retrieved from the table mysql.event,
|
|
reconstruct the CREATE PROCEDURE statement, parse it and create
|
|
an instance of the class Sys_trigger that encapsulate all
|
|
trigger-specific stuff including sp_head.
|
|
|
|
@param thd Thread handler
|
|
@param db_name database name where the trigger is defined
|
|
@param trg_name trigger name
|
|
@param trg_definer trigger definer
|
|
@param trg_body trigger body
|
|
@param sql_mode sql_mode used on trigger creation
|
|
@param ctx creation context
|
|
@param[out] output variable to store parsing result:
|
|
false on success, true on error
|
|
|
|
@return a pointer to the instance of the class Sys_trigger on success,
|
|
null_ptr on error
|
|
*/
|
|
|
|
static Sys_trigger *
|
|
instantiate_sys_trigger(THD *thd,
|
|
const LEX_CSTRING&db_name,
|
|
const LEX_CSTRING &trg_name,
|
|
const LEX_STRING &trg_definer,
|
|
const LEX_STRING &trg_body,
|
|
sql_mode_t sql_mode,
|
|
Stored_program_creation_ctx *ctx,
|
|
bool *parse_error)
|
|
{
|
|
/*
|
|
Buffer for storing CREATE PROCEDURE statement constructed for
|
|
system trigger on loading trigger's metadata from mysql.event table
|
|
*/
|
|
String create_sp_stmt;
|
|
|
|
Sql_mode_instant_set guard(thd, sql_mode);
|
|
|
|
/*
|
|
System triggers in runtime is treated as stored procedures, so
|
|
construct the CREATE PROCEDURE statement based on metadata
|
|
retrieved for the system trigger from the table mysql.event.
|
|
*/
|
|
if (construct_sp_sql_for_sys_trigger(thd, &create_sp_stmt,
|
|
trg_name, trg_body))
|
|
return nullptr;
|
|
|
|
Sys_trigger *sys_trg= nullptr;
|
|
|
|
sp_head *sp= compile_trigger_stmt(thd, db_name, &create_sp_stmt, ctx,
|
|
parse_error);
|
|
if (sp)
|
|
{
|
|
sys_trg=
|
|
new (thd_for_sys_triggers->mem_root) Sys_trigger(thd_for_sys_triggers,
|
|
sp);
|
|
|
|
if (likely(sys_trg != nullptr))
|
|
{
|
|
sp->m_sql_mode= sql_mode;
|
|
sp->set_creation_ctx(ctx);
|
|
sp->optimize();
|
|
|
|
sp->set_definer(trg_definer.str, trg_definer.length);
|
|
}
|
|
else
|
|
sp_head::destroy(sp);
|
|
}
|
|
|
|
create_sp_stmt.free();
|
|
|
|
return sys_trg;
|
|
}
|
|
|
|
|
|
/**
|
|
Do loading of a system trigger from the next record fetched from the table
|
|
mysql.event for event types different from SCHEDULE_EVENT, that is from
|
|
the record storing system trigger's metadata
|
|
|
|
@param thd Thread handler
|
|
@param event_table Opened table mysql.event
|
|
@param[out] db_name database name
|
|
@param[out] trg_name trigger name
|
|
@param[out] trg_body trigger body
|
|
@param[out] trg_definer trigger definer
|
|
@param[out] sql_mode sql_mode used on trigger creation
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
static bool load_trigger_metadata(THD *thd, TABLE *event_table,
|
|
LEX_STRING *db_name,
|
|
LEX_CSTRING *trg_name,
|
|
LEX_STRING *trg_body,
|
|
LEX_STRING *trg_definer,
|
|
sql_mode_t *sql_mode,
|
|
longlong *trg_when,
|
|
Stored_program_creation_ctx **creation_ctx)
|
|
{
|
|
*db_name=
|
|
event_table->field[ET_FIELD_DB]->val_lex_string_strmake(thd->mem_root);
|
|
if (db_name->str == nullptr)
|
|
return true;
|
|
|
|
*trg_name=
|
|
event_table->field[ET_FIELD_NAME]->val_lex_string_strmake(thd->mem_root);
|
|
if (trg_name->str == nullptr)
|
|
return true;
|
|
|
|
*trg_body=
|
|
event_table->field[ET_FIELD_BODY]->val_lex_string_strmake(thd->mem_root);
|
|
if (trg_body->str == nullptr)
|
|
return true;
|
|
|
|
*trg_definer=
|
|
event_table->field[ET_FIELD_DEFINER]->val_lex_string_strmake(thd->mem_root);
|
|
|
|
if (trg_definer->str == nullptr)
|
|
return true;
|
|
|
|
*sql_mode= (sql_mode_t) event_table->field[ET_FIELD_SQL_MODE]->val_int();
|
|
|
|
(void)Event_creation_ctx::load_from_db(thd, thd->mem_root,
|
|
db_name->str, trg_name->str,
|
|
event_table,
|
|
creation_ctx);
|
|
|
|
*trg_when=
|
|
(trg_action_time_type)(event_table->field[ET_FIELD_WHEN]->val_int());
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
events for system triggers stored in the column mysql.event.kind
|
|
has the following values:
|
|
SCHEDULE = 0x01
|
|
STARTUP = 0x02
|
|
SHUTDOWN = 0x04
|
|
LOGON = 0x08
|
|
LOGOFF = 0x10
|
|
DDL = 0x20
|
|
|
|
The following function checks the event kind for validness,
|
|
meaning that no bits are set except ones listed above.
|
|
All above bits set is also considered as invalid since
|
|
the bit SCHEDULE and others ones are mutually exclusive.
|
|
*/
|
|
|
|
static inline bool is_trg_kind_invalid(longlong trg_event)
|
|
{
|
|
constexpr longlong ALL_BITS= 0x3F;
|
|
constexpr longlong SCHEDULE_BIT= 0x01;
|
|
constexpr longlong SYS_TRG_BITS= 0x3E;
|
|
|
|
return
|
|
/*
|
|
No bits is set for trigger event that considered
|
|
as error condition
|
|
*/
|
|
trg_event == 0 ||
|
|
/*
|
|
All bits is set, i.e
|
|
SCHEDULE | STARTUP | SHUTDOWN | LOGON | LOGOFF | DDL
|
|
that is invalid case
|
|
*/
|
|
trg_event == ALL_BITS ||
|
|
/*
|
|
No bits to the left from the most
|
|
significant DDL bit can be set
|
|
*/
|
|
((trg_event) & ~ALL_BITS) != 0 ||
|
|
/*
|
|
SCHEDULE bit and any of STARTUP/SHUTDOWN/LOGON/LOGOFF/DDL
|
|
can't be set together since they are mutually exclusive
|
|
*/
|
|
(((trg_event & SCHEDULE_BIT) != 0) && ((trg_event & SYS_TRG_BITS) != 0));
|
|
}
|
|
|
|
|
|
/**
|
|
Convert the value for a trigger event time (BEFORE, AFTER) from
|
|
format used for storing in the table mysql.event to host representation
|
|
used in runtime.
|
|
|
|
@param when The original value retrieved from the column
|
|
mysql.event.when
|
|
|
|
@return the value converted to the type trg_action_time_type
|
|
*/
|
|
|
|
static inline trg_action_time_type convert_to_action_time_enum(longlong when)
|
|
{
|
|
/*
|
|
trigger event time is stored in the mysql.event table in the column
|
|
`when` declared as enum('BEFORE','AFTER'). So, enumerators has
|
|
the following values: 1 for `BEFORE`, 2 for `AFTER`.
|
|
On the other hand, the enum trg_action_time_type has values starting
|
|
from 0, so adjust values restored from the table mysql.event before using
|
|
them for calculations.
|
|
*/
|
|
return (trg_action_time_type)(when - 1);
|
|
}
|
|
|
|
/**
|
|
Check the trigger kind and trigger time of action for validity.
|
|
Form an error message in case values of trigger kind or time is not valid
|
|
and buffer for error message (represented by the parameter err_msg)
|
|
is not null.
|
|
|
|
@param err_msg[out] buffer where to write the error message in case error
|
|
@param cur_msg_len length of message in the buffer err_msg
|
|
@param err_msg_buf_sz size of buffer for an error message
|
|
@param trg_kind value of a trigger kind to check for validity
|
|
@param trg_when value of a trigger action time to check for validity
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
static bool check_valid_trigger_metadata(char *err_msg,
|
|
size_t cur_msg_len,
|
|
size_t err_msg_buf_sz,
|
|
longlong trg_kind,
|
|
longlong trg_when)
|
|
{
|
|
bool ret= false;
|
|
|
|
if (is_trg_kind_invalid(trg_kind))
|
|
{
|
|
if (err_msg)
|
|
{
|
|
DBUG_ASSERT(cur_msg_len == 0);
|
|
cur_msg_len+= snprintf(err_msg, err_msg_buf_sz,
|
|
"Invalid event.kind value %lld",
|
|
trg_kind);
|
|
ret= true;
|
|
}
|
|
else
|
|
return true;
|
|
}
|
|
|
|
switch (convert_to_action_time_enum(trg_when))
|
|
{
|
|
case TRG_ACTION_BEFORE:
|
|
case TRG_ACTION_AFTER:
|
|
break;
|
|
default:
|
|
if (err_msg)
|
|
{
|
|
if (cur_msg_len)
|
|
snprintf(err_msg + cur_msg_len,
|
|
err_msg_buf_sz - cur_msg_len,
|
|
", Invalid event.when value %lld",
|
|
trg_when);
|
|
else
|
|
snprintf(err_msg, err_msg_buf_sz,
|
|
"Invalid event.when value %lld",
|
|
trg_when);
|
|
ret= true;
|
|
}
|
|
else
|
|
return true;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
/**
|
|
Check the trigger kind and trigger time of action for validity.
|
|
Raise the error in diagnostics area in case the trigger metadata
|
|
is not valid.
|
|
|
|
@param trg_name the trigger name
|
|
@param trg_kind value of a trigger kind to check for validity
|
|
@param trg_status trigger status value to check for validity
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
static bool check_valid_trigger_metadata(const LEX_STRING &db_name,
|
|
const LEX_CSTRING &trg_name,
|
|
longlong trg_kind, longlong trg_when)
|
|
{
|
|
bool ret;
|
|
char err_msg[MYSQL_ERRMSG_SIZE];
|
|
|
|
err_msg[0]= 0;
|
|
ret= check_valid_trigger_metadata(err_msg, 0, sizeof(err_msg),
|
|
trg_kind, trg_when);
|
|
if (ret)
|
|
my_error(ER_SYSTEM_TRG_INVALID_METADATA, MYF(0),
|
|
db_name.str, trg_name.str, err_msg);
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
/**
|
|
Load system triggers of the specified type from the table mysql.event
|
|
|
|
@param thd Thread handler
|
|
@param trg_action_time action time of trigger (BEFORE, AFTER) that is
|
|
requested by the caller
|
|
@param trg_type_to_load type of system triggers to load (ON STARTUP,
|
|
ON SHUTDOWN, etc)
|
|
@param[out] triggers_list the list of Sys_trigger objects to execute for
|
|
specified combination of trigger
|
|
'action time'/type
|
|
|
|
@return false on success, true on error
|
|
@note failure on opening the table mysql.event is not considered as error
|
|
*/
|
|
|
|
static bool load_system_triggers(THD *thd,
|
|
trg_action_time_type trg_action_time,
|
|
Event_parse_data::enum_kind trg_type_to_load,
|
|
List<Sys_trigger> *triggers_list)
|
|
{
|
|
TABLE *event_table;
|
|
start_new_trans new_trans(thd);
|
|
bool sys_triggers_enabled;
|
|
|
|
if (Event_db_repository::open_event_table(thd, TL_READ, &event_table,
|
|
&sys_triggers_enabled))
|
|
{
|
|
my_message(ER_STARTUP,
|
|
"An error occurred when loading data from "
|
|
"the table mysql.event. System triggers not loaded",
|
|
MYF(ME_ERROR_LOG));
|
|
|
|
return false;
|
|
}
|
|
|
|
if (sys_triggers_enabled == false)
|
|
{
|
|
close_thread_tables(thd);
|
|
return false;
|
|
}
|
|
|
|
READ_RECORD read_record_info;
|
|
if (init_read_record(&read_record_info, thd, event_table,
|
|
nullptr, nullptr, 0, 1, false))
|
|
{
|
|
close_thread_tables(thd);
|
|
return true;
|
|
}
|
|
|
|
bool ret= false;
|
|
|
|
while (!(read_record_info.read_record()))
|
|
{
|
|
Event_parse_data::enum_status trg_status;
|
|
sql_mode_t sql_mode;
|
|
|
|
longlong trg_kind_in= event_table->field[ET_FIELD_KIND]->val_int();
|
|
|
|
/*
|
|
Skip records for real events (not triggers)
|
|
*/
|
|
if ((Event_parse_data::enum_kind)trg_kind_in ==
|
|
Event_parse_data::SCHEDULE_EVENT)
|
|
continue;
|
|
|
|
/*
|
|
Skip records not matching the requested trigger type
|
|
*/
|
|
if ((trg_kind_in & trg_type_to_load) == 0)
|
|
continue;
|
|
|
|
trg_status= (Event_parse_data::enum_status)
|
|
event_table->field[ET_FIELD_STATUS]->val_int();
|
|
/*
|
|
Skip records for disabled triggers
|
|
*/
|
|
if (trg_status != Event_parse_data::ENABLED)
|
|
continue;
|
|
|
|
LEX_CSTRING trg_name= null_clex_str;
|
|
LEX_STRING db_name= empty_lex_str, trg_body= empty_lex_str,
|
|
trg_definer= empty_lex_str;
|
|
longlong trg_when_in;
|
|
Stored_program_creation_ctx *creation_ctx;
|
|
if (load_trigger_metadata(thd, event_table, &db_name, &trg_name,
|
|
&trg_body, &trg_definer, &sql_mode,
|
|
&trg_when_in,
|
|
&creation_ctx))
|
|
{
|
|
sql_print_error("Fail to load metadata for trigger %s.%s [%s]. Skip it.",
|
|
(db_name.str ? db_name.str : "\"null\""),
|
|
(trg_name.str ? trg_name.str : "\"null\""),
|
|
thd->get_stmt_da()->message());
|
|
|
|
/*
|
|
Skip triggers with invalid data fetched from the table mysql.event
|
|
*/
|
|
continue;
|
|
}
|
|
|
|
char err_msg[MYSQL_ERRMSG_SIZE];
|
|
err_msg[0]= 0;
|
|
if (check_valid_trigger_metadata(err_msg, 0, sizeof(err_msg),
|
|
trg_kind_in, trg_when_in))
|
|
{
|
|
sql_print_error("Fail to load metadata for trigger %s.%s [%s]. Skip it.",
|
|
(db_name.str ? db_name.str : "\"null\""),
|
|
(trg_name.str ? trg_name.str : "\"null\""),
|
|
err_msg);
|
|
/*
|
|
Skip triggers with invalid metadata (assuming that somebody updated
|
|
the trigger metadata explicitly bypassing the SQL interface (i.e.,
|
|
the trigger metadata is set explicitly with the statement
|
|
`UPDATE mysql.event SET ...`))
|
|
*/
|
|
continue;
|
|
}
|
|
|
|
trg_action_time_type trg_when= convert_to_action_time_enum(trg_when_in);
|
|
/*
|
|
Skip triggers not matching requested action time
|
|
*/
|
|
if (trg_when != trg_action_time)
|
|
continue;
|
|
|
|
bool parse_error= false;
|
|
Sys_trigger *sys_trg=
|
|
instantiate_sys_trigger(thd, db_name, trg_name,
|
|
trg_definer,
|
|
trg_body, sql_mode, creation_ctx,
|
|
&parse_error);
|
|
|
|
if (parse_error)
|
|
{
|
|
sql_print_error("Fail to load metadata for trigger %s.%s [%s] Skip it.",
|
|
db_name.str, trg_name.str,
|
|
thd->get_stmt_da()->message());
|
|
thd->clear_error();
|
|
|
|
/*
|
|
Skip triggers containing non-parsable body, probably updated
|
|
intentionally for some records in the table mysql.event. Doing this way
|
|
we allow to start server even on presence of unparseable triggers
|
|
*/
|
|
continue;
|
|
}
|
|
|
|
if (sys_trg == nullptr ||
|
|
triggers_list->push_back(sys_trg, thd->mem_root))
|
|
{
|
|
/* OOM error happened */
|
|
ret= true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
end_read_record(&read_record_info);
|
|
thd->commit_whole_transaction_and_close_tables();
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
/**
|
|
Initialize THD used for handling System triggers ON STARTUP/ON SHUTDOWN
|
|
*/
|
|
|
|
static void init_thd_for_on_startup_shutdown_triggers(void *stack_top)
|
|
{
|
|
if (thd_for_sys_triggers == nullptr)
|
|
{
|
|
/*
|
|
operator new() for the class THD_PROCESS_WIDE invokes my_malloc
|
|
for memory allocaiton and passes it the flag MY_FAE that forces
|
|
calling abort() in case OOM error. It means that thd_for_sys_triggers
|
|
never equals nullptr.
|
|
*/
|
|
thd_for_sys_triggers= new THD_PROCESS_WIDE{0};
|
|
DBUG_ASSERT(thd_for_sys_triggers != nullptr);
|
|
thd_for_sys_triggers->store_globals();
|
|
thd_for_sys_triggers->set_query_inner(
|
|
(char*) STRING_WITH_LEN("load_system_triggers"),
|
|
default_charset_info);
|
|
thd_for_sys_triggers->set_time();
|
|
|
|
/*
|
|
Turn off read only mode for THD dedicated to handling system triggers
|
|
*/
|
|
thd_for_sys_triggers->security_ctx->master_access.force_allow(
|
|
PRIV_IGNORE_READ_ONLY, true);
|
|
thd_for_sys_triggers->tx_read_only= false;
|
|
thd_for_sys_triggers->variables.tx_read_only= false;
|
|
lex_start(thd_for_sys_triggers);
|
|
}
|
|
else
|
|
/*
|
|
In case init_thd_for_on_startup_shutdown_triggers() is called
|
|
the second time, the global variable thd_for_sys_triggers has been
|
|
already set on loading trigger on server starting up, therefore
|
|
thd_for_sys_triggers to current_thd in order to have not null value,
|
|
so that any assert checking for condition
|
|
table->in_use == _current_thd()
|
|
be satisfied
|
|
*/
|
|
set_current_thd(thd_for_sys_triggers);
|
|
|
|
thd_for_sys_triggers->thread_stack= stack_top;
|
|
}
|
|
|
|
|
|
/**
|
|
Run system triggers for specified action time and event type, if any.
|
|
|
|
@param triggers_list List of triggers for run
|
|
@param action_time Time when trigger is invoked (before or after)
|
|
@param event_type Type of trigger to fire (ON STARTUP, ON SHUTDOWN)
|
|
*/
|
|
|
|
static void
|
|
run_sys_triggers(List<Sys_trigger> &triggers_list,
|
|
enum trg_action_time_type action_time,
|
|
trg_sys_event_type event_type)
|
|
{
|
|
if (triggers_list.is_empty())
|
|
return;
|
|
|
|
const bool do_output_info= (global_system_variables.log_warnings);
|
|
const LEX_CSTRING &action_type_name=
|
|
base_event_names[event_type - TRG_EVENT_STARTUP + 1];
|
|
const LEX_CSTRING &action_time_name= base_event_time[action_time];
|
|
|
|
if (do_output_info)
|
|
sql_print_information("Start execution of %s %s triggers",
|
|
action_time_name.str, action_type_name.str);
|
|
List_iterator_fast<Sys_trigger> iter(triggers_list);
|
|
Sys_trigger *trg;
|
|
while ((trg= iter++))
|
|
{
|
|
/*
|
|
Ignore errors that could happen on running any of 'system' triggers
|
|
in order to at least be able to start the server in case any of
|
|
on start up trigger failing regardless of failure reason
|
|
*/
|
|
(void)trg->execute();
|
|
}
|
|
|
|
if (do_output_info)
|
|
sql_print_information("End execution of %s %s triggers",
|
|
action_time_name.str, action_type_name.str);
|
|
}
|
|
|
|
|
|
/**
|
|
Release any resource allocated in runtime for support of system triggers.
|
|
*/
|
|
|
|
void release_resources(THD *original_thd)
|
|
{
|
|
close_thread_tables(thd_for_sys_triggers);
|
|
lex_end_nops(thd_for_sys_triggers->lex);
|
|
delete thd_for_sys_triggers;
|
|
thd_for_sys_triggers= nullptr;
|
|
|
|
set_current_thd(original_thd);
|
|
}
|
|
|
|
|
|
/**
|
|
Load ON STARTUP triggers from the table mysql.event and run them
|
|
|
|
@param bootstrap_or_noacl true in case server is run either with bootstrap
|
|
or skip_grant_table option, else false
|
|
*/
|
|
|
|
void run_after_startup_triggers(bool bootstrap_or_noacl)
|
|
{
|
|
if (bootstrap_or_noacl)
|
|
return;
|
|
|
|
bool stack_top;
|
|
|
|
THD *original_thd= current_thd;
|
|
init_thd_for_on_startup_shutdown_triggers(&stack_top);
|
|
|
|
List<Sys_trigger> triggers_list;
|
|
if (load_system_triggers(thd_for_sys_triggers,
|
|
TRG_ACTION_AFTER,
|
|
Event_parse_data::SYS_TRG_ON_STARTUP,
|
|
&triggers_list))
|
|
{
|
|
sql_print_error("Fail to load after startup triggers metadata from "
|
|
"the table mysql.event [%s]",
|
|
thd_for_sys_triggers->get_stmt_da()->message());
|
|
triggers_list.delete_elements();
|
|
release_resources(original_thd);
|
|
return;
|
|
}
|
|
|
|
run_sys_triggers(triggers_list, TRG_ACTION_AFTER, TRG_EVENT_STARTUP);
|
|
triggers_list.delete_elements();
|
|
thd_for_sys_triggers->thread_stack= nullptr;
|
|
set_current_thd(original_thd);
|
|
}
|
|
|
|
|
|
/**
|
|
Run ON SHUTDOWN triggers
|
|
|
|
@param bootstrap_or_noacl true in case server is run either with bootstrap
|
|
or skip_grant_table option, else false
|
|
*/
|
|
|
|
void run_before_shutdown_triggers(bool bootstrap_or_noacl)
|
|
{
|
|
if (bootstrap_or_noacl)
|
|
return;
|
|
|
|
bool stack_top;
|
|
THD *original_thd= current_thd;
|
|
|
|
init_thd_for_on_startup_shutdown_triggers(&stack_top);
|
|
|
|
List<Sys_trigger> triggers_list;
|
|
if (load_system_triggers(thd_for_sys_triggers,
|
|
TRG_ACTION_BEFORE,
|
|
Event_parse_data::SYS_TRG_ON_SHUTDOWN,
|
|
&triggers_list))
|
|
{
|
|
sql_print_error("Fail to load before shutdown triggers metadata from "
|
|
"the table mysql.event [%s]",
|
|
thd_for_sys_triggers->get_stmt_da()->message());
|
|
triggers_list.delete_elements();
|
|
release_resources(original_thd);
|
|
return;
|
|
}
|
|
|
|
run_sys_triggers(triggers_list, TRG_ACTION_BEFORE, TRG_EVENT_SHUTDOWN);
|
|
|
|
triggers_list.delete_elements();
|
|
release_resources(original_thd);
|
|
}
|
|
|
|
|
|
static bool send_show_create_trigger_result(
|
|
THD *thd, MEM_ROOT *mem_root,
|
|
Protocol *protocol,
|
|
const LEX_CSTRING &trg_name,
|
|
const LEX_CSTRING &trg_sql_mode,
|
|
const LEX_CSTRING &trg_create_sql_stmt,
|
|
MYSQL_TIME created,
|
|
CHARSET_INFO *client_cs,
|
|
CHARSET_INFO *connection_cl,
|
|
CHARSET_INFO *db_cl)
|
|
{
|
|
if (send_show_create_trigger_metadata(thd, mem_root, protocol,
|
|
trg_sql_mode, trg_create_sql_stmt))
|
|
return true;
|
|
|
|
protocol->prepare_for_resend();
|
|
|
|
protocol->store(trg_name.str,
|
|
trg_name.length,
|
|
system_charset_info);
|
|
|
|
protocol->store(trg_sql_mode.str,
|
|
trg_sql_mode.length,
|
|
system_charset_info);
|
|
|
|
protocol->store(trg_create_sql_stmt.str,
|
|
trg_create_sql_stmt.length,
|
|
client_cs);
|
|
|
|
protocol->store(&client_cs->cs_name, system_charset_info);
|
|
|
|
protocol->store(&connection_cl->coll_name, system_charset_info);
|
|
|
|
protocol->store(&db_cl->coll_name, system_charset_info);
|
|
|
|
protocol->store_datetime(&created, 2);
|
|
|
|
bool ret= protocol->write();
|
|
|
|
if (!ret)
|
|
my_eof(thd);
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
static LEX_CSTRING
|
|
convert_to_quoted_definer(THD *thd, StringBuffer<1024> *quoted_definer,
|
|
const LEX_CSTRING &definer)
|
|
{
|
|
const char *delimiter= strchr(definer.str, '@');
|
|
if (delimiter)
|
|
{
|
|
if (delimiter > definer.str)
|
|
{
|
|
LEX_CSTRING orig_user{definer.str, size_t(delimiter - definer.str)};
|
|
append_identifier(thd, quoted_definer, &orig_user);
|
|
const size_t host_len= (definer.length -
|
|
/* subtract the length of user part */
|
|
(delimiter - definer.str) -
|
|
/* subtract 1 for the symbol @ */
|
|
1);
|
|
LEX_CSTRING orig_host{delimiter + 1, host_len};
|
|
append_at_host(thd, quoted_definer, &orig_host);
|
|
}
|
|
else
|
|
{
|
|
LEX_CSTRING orig_host{definer.str + 1, definer.length - 1};
|
|
append_at_host(thd, quoted_definer, &orig_host);
|
|
}
|
|
}
|
|
else
|
|
append_identifier(thd, quoted_definer, &definer);
|
|
|
|
return quoted_definer->to_lex_cstring();
|
|
}
|
|
|
|
|
|
/**
|
|
Implementation of SHOW CREATE TRIGGER statement for system triggers
|
|
|
|
@param thd Thread context
|
|
@param trg_name Trigger name specified for SHOW CREATE TRIGGER statement
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
bool show_create_sys_trigger(THD *thd, const sp_name *trg_name)
|
|
{
|
|
TABLE *event_table;
|
|
/*
|
|
Activate the guard to restore original sql_mode
|
|
on any return path from this function.
|
|
*/
|
|
Transaction_Resources_Guard transaction_guard{thd};
|
|
|
|
bool sys_triggers_enabled;
|
|
if (Event_db_repository::open_event_table(thd, TL_READ, &event_table,
|
|
&sys_triggers_enabled))
|
|
return true;
|
|
|
|
if (!sys_triggers_enabled)
|
|
{
|
|
/*
|
|
The variable sys_triggers_enabled has the value false in case
|
|
the table mysql.event lacks some columns required for support of
|
|
system triggers. It is assumed that this use case is possible when
|
|
MariaDB server with support of system triggers is started against
|
|
database created by prior version of server w/o such support.
|
|
In this case raise the error ER_TRG_DOES_NOT_EXIST since a system
|
|
trigger can't exist in such database
|
|
*/
|
|
my_error(ER_TRG_DOES_NOT_EXIST, MYF(0));
|
|
return true;
|
|
}
|
|
|
|
bool has_event_with_the_same_name= false;
|
|
bool found __attribute__((unused));
|
|
/*
|
|
Set up the search key and look up the record in mysql.event for
|
|
the trigger.
|
|
*/
|
|
if (fetch_trigger_record_by_name(event_table, trg_name,
|
|
&found,
|
|
&has_event_with_the_same_name,
|
|
REPORT_TRG_DOES_NOT_EXIST_ERROR))
|
|
return true;
|
|
|
|
/*
|
|
Trigger doesn't exist in case the record is for the real event
|
|
*/
|
|
if (has_event_with_the_same_name)
|
|
{
|
|
my_error(ER_TRG_DOES_NOT_EXIST, MYF(0));
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
Since the SUPER_ACL privilege is required to create system triggers,
|
|
request the same privilege for SHOW CREATE TRIGGER. Do checking for
|
|
access rights after presence of the trigger assured, else
|
|
ER_SPECIFIC_ACCESS_DENIED_ERROR be emitted instead of ER_TRG_DOES_NOT_EXIST
|
|
that misleading in case a user mistyping the DML trigger name.
|
|
*/
|
|
if (check_global_access(thd, SUPER_ACL))
|
|
return true;
|
|
|
|
LEX_STRING db_name, trg_body, trg_definer;
|
|
LEX_CSTRING trigger_name{(char*)trg_name->m_name.str,
|
|
trg_name->m_name.length};
|
|
sql_mode_t sql_mode;
|
|
longlong trg_when_in;
|
|
Stored_program_creation_ctx *creation_ctx;
|
|
if (load_trigger_metadata(thd, event_table, &db_name, &trigger_name,
|
|
&trg_body, &trg_definer, &sql_mode, &trg_when_in,
|
|
&creation_ctx))
|
|
{
|
|
my_error(ER_CANNOT_LOAD_FROM_TABLE_V2, MYF(0), "mysql", "event");
|
|
return true;
|
|
}
|
|
|
|
longlong trg_kind_in=
|
|
(Event_parse_data::enum_kind)event_table->field[ET_FIELD_KIND]->val_int();
|
|
|
|
if (check_valid_trigger_metadata(db_name, trigger_name,
|
|
trg_kind_in, trg_when_in))
|
|
return true;
|
|
/*
|
|
Convert a definer value to the quoted form `user`@`host` to align with
|
|
output of SHOW CREATE TRIGGER for DML triggers
|
|
*/
|
|
StringBuffer<1024> quoted_definer;
|
|
convert_to_quoted_definer(thd, "ed_definer, trg_definer);
|
|
|
|
String create_trg_stmt;
|
|
trg_action_time_type trg_when= convert_to_action_time_enum(trg_when_in);
|
|
|
|
if (reconstruct_create_trigger_stmt(thd, &create_trg_stmt,
|
|
quoted_definer.to_lex_cstring(),
|
|
trigger_name,
|
|
(Event_parse_data::enum_kind)trg_kind_in,
|
|
trg_when,
|
|
trg_body))
|
|
return true;
|
|
|
|
LEX_CSTRING trg_sql_mode_str;
|
|
sql_mode_string_representation(thd, sql_mode, &trg_sql_mode_str);
|
|
|
|
ulonglong created= event_table->field[ET_FIELD_CREATED]->val_int();
|
|
MYSQL_TIME created_timestamp;
|
|
int not_used=0;
|
|
number_to_datetime_or_date(created, 0, &created_timestamp, 0, ¬_used);
|
|
|
|
if (send_show_create_trigger_result(thd, thd->mem_root, thd->protocol,
|
|
trg_name->m_name,
|
|
trg_sql_mode_str,
|
|
create_trg_stmt.to_lex_cstring(),
|
|
created_timestamp,
|
|
creation_ctx->get_client_cs(),
|
|
creation_ctx->get_connection_cl(),
|
|
creation_ctx->get_db_cl()))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
Fill a record of information_schema.triggers for a system trigger
|
|
|
|
@param thd thread context
|
|
@param trg_name the trigger name
|
|
@param table TABLE object for the table information_schema.triggers
|
|
@param db_name the name of database where the trigger is defined
|
|
@param sql_mode sql_mode used on trigger creation
|
|
@param definer definer used on trigger creation
|
|
@param trg_body body of the trigger
|
|
@param trg_time trigger event time (BEFORE or AFTER)
|
|
@param trg_event trigger event (ON STARTUP, ON SHUTDOWN, etc)
|
|
@param created_timestamp date/time when the trigger created
|
|
@param client_cs_name client character set name
|
|
@param connection_cl_name connection collation name
|
|
@param db_cl_name database collation name
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
static bool store_sys_trigger(THD *thd, const LEX_CSTRING &trg_name,
|
|
TABLE *table, const LEX_STRING *db_name,
|
|
sql_mode_t sql_mode,
|
|
const LEX_STRING &definer,
|
|
const LEX_STRING &trg_body,
|
|
const LEX_CSTRING &trg_time,
|
|
const LEX_CSTRING &trg_event,
|
|
const MYSQL_TIME &created_timestamp,
|
|
const LEX_CSTRING &client_cs_name,
|
|
const LEX_CSTRING &connection_cl_name,
|
|
const LEX_CSTRING &db_cl_name)
|
|
{
|
|
CHARSET_INFO *cs= system_charset_info;
|
|
|
|
restore_record(table, s->default_values);
|
|
/* TRIGGER_CATALOG */
|
|
table->field[0]->store(STRING_WITH_LEN("def"), cs);
|
|
/* TRIGGER_SCHEMA */
|
|
table->field[1]->store(*db_name, cs);
|
|
/* TRIGGER_NAME */
|
|
table->field[2]->store(trg_name, cs);
|
|
/* EVENT_MANIPULATION for a system trigger (STARTUP, SHUTDOWN, DDL, etc) */
|
|
table->field[3]->store(trg_event, cs);
|
|
/* EVENT_OBJECT_CATALOG */
|
|
table->field[4]->store(STRING_WITH_LEN("def"), cs);
|
|
/* EVENT_OBJECT_SCHEMA */
|
|
table->field[5]->set_null();
|
|
/* EVENT_OBJECT_TABLE */
|
|
table->field[6]->set_null();
|
|
/* ACTION_ORDER */
|
|
table->field[7]->set_null(); // TODO: adjust!
|
|
/* ACTION_CONDITION */
|
|
table->field[8]->set_null();
|
|
/* ACTION_STATEMENT */
|
|
table->field[9]->set_notnull();
|
|
table->field[9]->store(trg_body, cs);
|
|
|
|
/* ACTION_ORIENTATION */
|
|
table->field[10]->store(STRING_WITH_LEN("STATEMENT"), cs);
|
|
/* ACTION_TIMING (BEFORE, AFTER) */
|
|
table->field[11]->store(trg_time, cs);
|
|
/* ACTION_REFERENCE_OLD_TABLE */
|
|
table->field[12]->set_null();
|
|
/* ACTION_REFERENCE_NEW_TABLE */
|
|
table->field[13]->set_null();
|
|
/* ACTION_REFERENCE_OLD_ROW */
|
|
table->field[14]->set_null();
|
|
/* ACTION_REFERENCE_NEW_ROW */
|
|
table->field[15]->set_null();
|
|
|
|
/* CREATED */
|
|
table->field[16]->set_notnull();
|
|
table->field[16]->store_time_dec(&created_timestamp, 2);
|
|
|
|
LEX_CSTRING sql_mode_rep;
|
|
sql_mode_string_representation(thd, sql_mode, &sql_mode_rep);
|
|
/* SQL_MODE */
|
|
table->field[17]->store(sql_mode_rep.str, sql_mode_rep.length, cs);
|
|
/* DEFINER */
|
|
table->field[18]->set_notnull();
|
|
table->field[18]->store(definer, cs);
|
|
/* CHARACTER_SET_CLIENT */
|
|
table->field[19]->store(client_cs_name, cs);
|
|
/* COLLATION_CONNECTION */
|
|
table->field[20]->store(connection_cl_name, cs);
|
|
/* DATABASE_COLLATION */
|
|
table->field[21]->store(db_cl_name, cs);
|
|
|
|
return schema_table_store_record(thd, table);
|
|
}
|
|
|
|
|
|
/**
|
|
Retrieve information about a system trigger from the table mysql.event
|
|
and store it in information schema
|
|
|
|
@param thd Thread handler
|
|
@param ev_table Opened table mysql.event
|
|
@param tables an instance of the struct TABLE_LIST for the table
|
|
information_schema.triggers
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
static bool retrieve_and_store_record(THD *thd, TABLE *ev_table,
|
|
TABLE_LIST *tables)
|
|
{
|
|
longlong kind_val=ev_table->field[ET_FIELD_KIND]->val_int();
|
|
|
|
if (kind_val == Event_parse_data::SCHEDULE_EVENT)
|
|
/* Skip records for regular event */
|
|
return false;
|
|
|
|
LEX_CSTRING trg_name;
|
|
LEX_STRING db_name, trg_body, trg_definer;
|
|
longlong trg_when_in;
|
|
sql_mode_t sql_mode;
|
|
if (load_trigger_metadata(thd, ev_table, &db_name, &trg_name, &trg_body,
|
|
&trg_definer, &sql_mode, &trg_when_in, nullptr))
|
|
{
|
|
// OOM condition
|
|
my_error(ER_OUT_OF_RESOURCES, MYF(0));
|
|
|
|
return true;
|
|
}
|
|
|
|
char err_detail[128];
|
|
|
|
if (check_valid_trigger_metadata(err_detail, 0, sizeof(err_detail),
|
|
kind_val, trg_when_in))
|
|
{
|
|
/*
|
|
This function is called as part of handling the statement
|
|
SHOW TRIGGERS or querying the table information_schema.TRIGGERS.
|
|
Output the warning in order to get the DBA a chance to find and drop
|
|
invalid trigger entry.
|
|
*/
|
|
push_warning_printf(thd,
|
|
Sql_condition::WARN_LEVEL_WARN,
|
|
ER_SYSTEM_TRG_INVALID_METADATA,
|
|
ER_THD(thd, ER_SYSTEM_TRG_INVALID_METADATA),
|
|
(const char *) db_name.str,
|
|
(const char *) trg_name.str,
|
|
err_detail);
|
|
|
|
return false;
|
|
}
|
|
|
|
Event_parse_data::enum_kind trg_kind=
|
|
(Event_parse_data::enum_kind) kind_val;
|
|
|
|
Event_parse_data::enum_status trg_status= (Event_parse_data::enum_status)
|
|
ev_table->field[ET_FIELD_STATUS]->val_int();
|
|
if (trg_status != Event_parse_data::ENABLED)
|
|
return false;
|
|
|
|
trg_action_time_type trg_when= convert_to_action_time_enum(trg_when_in);
|
|
|
|
const Lex_cstring client_cs_name(
|
|
ev_table->field[ET_FIELD_CHARACTER_SET_CLIENT]->val_lex_string_strmake(
|
|
thd->mem_root));
|
|
|
|
if (client_cs_name.str == nullptr)
|
|
return true;
|
|
|
|
const Lex_cstring connection_cs_name(
|
|
ev_table->field[ET_FIELD_COLLATION_CONNECTION]->val_lex_string_strmake(
|
|
thd->mem_root));
|
|
|
|
if (connection_cs_name.str == nullptr)
|
|
return true;
|
|
|
|
const Lex_cstring db_cs_name(
|
|
ev_table->field[ET_FIELD_DB_COLLATION]->val_lex_string_strmake(
|
|
thd->mem_root));
|
|
|
|
if (db_cs_name.str == nullptr)
|
|
return true;
|
|
|
|
ulonglong created= ev_table->field[ET_FIELD_CREATED]->val_int();
|
|
MYSQL_TIME created_timestamp;
|
|
int not_used=0;
|
|
number_to_datetime_or_date(created, 0, &created_timestamp, 0, ¬_used);
|
|
|
|
char event_names_buf[max_event_names_length + 1];
|
|
|
|
return store_sys_trigger(thd, trg_name,
|
|
tables->table,
|
|
&db_name,
|
|
sql_mode,
|
|
trg_definer,
|
|
trg_body,
|
|
base_event_time[trg_when],
|
|
events_to_string(base_event_names,
|
|
event_names_buf,
|
|
trg_kind),
|
|
created_timestamp,
|
|
client_cs_name,
|
|
connection_cs_name,
|
|
db_cs_name);
|
|
}
|
|
|
|
|
|
/**
|
|
Do full table scan of the table mysql.event to retrieve information about
|
|
a system trigger from the table mysql.event. Store retrieved data into
|
|
the table information_schema.triggers
|
|
|
|
@param thd Thread handler
|
|
@param ev_table Opened table mysql.event
|
|
@param tables an instance of the struct TABLE_LIST for the table
|
|
information_schema.triggers
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
static bool full_scan_mysql_event_table(THD *thd, TABLE *ev_table,
|
|
TABLE_LIST *tables)
|
|
{
|
|
READ_RECORD read_record_info;
|
|
if (init_read_record(&read_record_info, thd, ev_table,
|
|
nullptr, nullptr, 0, 1, false))
|
|
return true;
|
|
|
|
bool ret= false;
|
|
|
|
while (!(read_record_info.read_record()))
|
|
{
|
|
if (retrieve_and_store_record(thd, ev_table, tables))
|
|
return true;
|
|
}
|
|
|
|
end_read_record(&read_record_info);
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
/**
|
|
Do index scan of the table mysql.event to retrieve information about
|
|
a system trigger from the table mysql.event. Store retrieved data into
|
|
the table information_schema.triggers
|
|
|
|
@param thd Thread handler
|
|
@param ev_table Opened table mysql.event
|
|
@param tables an instance of the struct TABLE_LIST for the table
|
|
information_schema.triggers
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
static bool index_scan_mysql_event_table(THD *thd, TABLE *ev_table,
|
|
TABLE_LIST *tables)
|
|
{
|
|
KEY *key_info;
|
|
uint key_len;
|
|
uchar *key_buf;
|
|
|
|
const LEX_CSTRING *lexdb= &thd->lex->first_select_lex()->db;
|
|
if (!is_infoschema_db(lexdb) && !is_perfschema_db(lexdb) &&
|
|
check_access(thd, TRIGGER_ACL, lexdb->str, nullptr, nullptr, 0, 0))
|
|
return true;
|
|
|
|
int ret= ev_table->file->ha_index_init(0, 1);
|
|
|
|
if (ret)
|
|
{
|
|
ev_table->file->print_error(ret, MYF(0));
|
|
return true;
|
|
}
|
|
|
|
key_info= ev_table->key_info;
|
|
|
|
if (key_info->user_defined_key_parts == 0 ||
|
|
key_info->key_part[0].field != ev_table->field[ET_FIELD_DB])
|
|
{
|
|
/* Corrupted table: no index or index on a wrong column */
|
|
my_error(ER_CANNOT_LOAD_FROM_TABLE_V2, MYF(0), "mysql", "event");
|
|
ev_table->file->ha_index_end();
|
|
|
|
return true;
|
|
}
|
|
|
|
const DBNameBuffer db_tmp(*lexdb, lower_case_table_names);
|
|
const char *db= db_tmp.to_lex_cstring().str;
|
|
|
|
ev_table->field[ET_FIELD_DB]->store(db, strlen(db), system_charset_info);
|
|
key_len= key_info->key_part[0].store_length;
|
|
|
|
if (!(key_buf= (uchar *)alloc_root(thd->mem_root, key_len)))
|
|
{
|
|
ev_table->file->ha_index_end();
|
|
|
|
return true;
|
|
}
|
|
|
|
key_copy(key_buf, ev_table->record[0], key_info, key_len);
|
|
ret= ev_table->file->ha_index_read_map(ev_table->record[0],
|
|
key_buf,
|
|
(key_part_map)1,
|
|
HA_READ_KEY_EXACT);
|
|
if (!ret)
|
|
{
|
|
do
|
|
{
|
|
if (retrieve_and_store_record(thd, ev_table, tables))
|
|
{
|
|
ev_table->file->ha_index_end();
|
|
|
|
return true;
|
|
}
|
|
|
|
ret= ev_table->file->ha_index_next_same(ev_table->record[0],
|
|
key_buf, key_len);
|
|
}
|
|
while (ret == 0);
|
|
}
|
|
|
|
/* ret is guaranteed to be != 0 */
|
|
if (ret == HA_ERR_END_OF_FILE || ret == HA_ERR_KEY_NOT_FOUND)
|
|
ret= 0;
|
|
else
|
|
ev_table->file->print_error(ret, MYF(0));
|
|
|
|
ev_table->file->ha_index_end();
|
|
|
|
return ret != 0;
|
|
}
|
|
|
|
|
|
/**
|
|
Fill in the table information_schema.triggers with data about existing
|
|
system triggers based on the data stored in the table mysql.event.
|
|
|
|
@param thd thread handler
|
|
@param tables an instance of the struct TABLE_LIST for the table
|
|
information_schema.triggers
|
|
|
|
@return false on success, true on error
|
|
*/
|
|
|
|
bool fill_schema_triggers_from_mysql_events(THD *thd, TABLE_LIST *tables)
|
|
{
|
|
/*
|
|
Check for permissions to query mysql database before searching
|
|
system triggers; do it silently (no_errors=true) in order to don't break
|
|
the whole statement SHOW TRIGGERS in case there are also DML triggers
|
|
*/
|
|
if (check_access(thd, SELECT_ACL, MYSQL_SCHEMA_NAME.str, NULL, NULL, 0,
|
|
true))
|
|
return false;
|
|
|
|
start_new_trans new_trans(thd);
|
|
|
|
bool sys_triggers_enabled;
|
|
TABLE *ev_table;
|
|
|
|
Query_tables_list query_tables_list_backup;
|
|
thd->lex->reset_n_backup_query_tables_list(&query_tables_list_backup);
|
|
|
|
auto scope_cleaner = make_scope_exit(
|
|
[&thd, &query_tables_list_backup]()
|
|
{
|
|
thd->commit_whole_transaction_and_close_tables();
|
|
|
|
thd->lex->restore_backup_query_tables_list(&query_tables_list_backup);
|
|
}
|
|
);
|
|
|
|
if (Event_db_repository::open_event_table(thd, TL_READ, &ev_table,
|
|
&sys_triggers_enabled))
|
|
return true;
|
|
|
|
if (!sys_triggers_enabled)
|
|
{
|
|
/*
|
|
ET_FIELD_KIND, ET_FIELD_WHEN accessed later in the 'while' loop
|
|
are missed on old mysql.event. In this case just return from
|
|
the function.
|
|
*/
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
lexdb == null_clex_str in case executing a direct query from
|
|
information_schema.tiggers. For SHOW TRIGGERS lexdb != null_clex_str
|
|
*/
|
|
if (thd->lex->first_select_lex()->db.str == nullptr)
|
|
return full_scan_mysql_event_table(thd, ev_table, tables);
|
|
else
|
|
return index_scan_mysql_event_table(thd, ev_table, tables);
|
|
}
|