diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 18:07:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 18:07:14 +0000 |
commit | a175314c3e5827eb193872241446f2f8f5c9d33c (patch) | |
tree | cd3d60ca99ae00829c52a6ca79150a5b6e62528b /sql/event_data_objects.cc | |
parent | Initial commit. (diff) | |
download | mariadb-10.5-upstream/1%10.5.12.tar.xz mariadb-10.5-upstream/1%10.5.12.zip |
Adding upstream version 1:10.5.12.upstream/1%10.5.12upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | sql/event_data_objects.cc | 1620 |
1 files changed, 1620 insertions, 0 deletions
diff --git a/sql/event_data_objects.cc b/sql/event_data_objects.cc new file mode 100644 index 00000000..a5e23261 --- /dev/null +++ b/sql/event_data_objects.cc @@ -0,0 +1,1620 @@ +/* + Copyright (c) 2005, 2010, Oracle and/or its affiliates. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +#define MYSQL_LEX 1 +#include "mariadb.h" /* NO_EMBEDDED_ACCESS_CHECKS */ +#include "sql_priv.h" +#include "unireg.h" +#include "sql_parse.h" // parse_sql +#include "strfunc.h" // find_string_in_array +#include "sql_db.h" // get_default_db_collation +#include "sql_time.h" // interval_type_to_name, + // date_add_interval, + // calc_time_diff +#include "tztime.h" // my_tz_find, my_tz_OFFSET0, struct Time_zone +#include "sp.h" // load_charset, load_collation +#include "events.h" +#include "event_data_objects.h" +#include "event_db_repository.h" +#include "sp_head.h" +#include "sql_show.h" // append_definer, append_identifier +#include "mysql/psi/mysql_sp.h" +#ifdef WITH_WSREP +#include "wsrep_trans_observer.h" +#endif /* WITH_WSREP */ +/** + @addtogroup Event_Scheduler + @{ +*/ + +#ifdef HAVE_PSI_INTERFACE +void init_scheduler_psi_keys() +{ + const char *category= "scheduler"; + + PSI_server->register_statement(category, & Event_queue_element_for_exec::psi_info, 1); +} + +PSI_statement_info Event_queue_element_for_exec::psi_info= +{ 0, "event", 0}; +#endif + +/*************************************************************************/ + +/** + Event_creation_ctx -- creation context of events. +*/ + +class Event_creation_ctx :public Stored_program_creation_ctx, + public Sql_alloc +{ +public: + static bool load_from_db(THD *thd, + MEM_ROOT *event_mem_root, + const char *db_name, + const char *event_name, + TABLE *event_tbl, + Stored_program_creation_ctx **ctx); + +public: + virtual Stored_program_creation_ctx *clone(MEM_ROOT *mem_root) + { + return new (mem_root) + Event_creation_ctx(m_client_cs, m_connection_cl, m_db_cl); + } + +protected: + virtual Object_creation_ctx *create_backup_ctx(THD *thd) const + { + /* + We can avoid usual backup/restore employed in stored programs since we + know that this is a top level statement and the worker thread is + allocated exclusively to execute this event. + */ + + return NULL; + } + +private: + Event_creation_ctx(CHARSET_INFO *client_cs, + CHARSET_INFO *connection_cl, + CHARSET_INFO *db_cl) + : Stored_program_creation_ctx(client_cs, connection_cl, db_cl) + { } +}; + +/************************************************************************** + Event_creation_ctx implementation. +**************************************************************************/ + +bool +Event_creation_ctx::load_from_db(THD *thd, + MEM_ROOT *event_mem_root, + const char *db_name, + const char *event_name, + TABLE *event_tbl, + Stored_program_creation_ctx **ctx) +{ + /* Load character set/collation attributes. */ + + CHARSET_INFO *client_cs; + CHARSET_INFO *connection_cl; + CHARSET_INFO *db_cl; + + bool invalid_creation_ctx= FALSE; + + if (load_charset(event_mem_root, + event_tbl->field[ET_FIELD_CHARACTER_SET_CLIENT], + thd->variables.character_set_client, + &client_cs)) + { + sql_print_warning("Event '%s'.'%s': invalid value " + "in column mysql.event.character_set_client.", + (const char *) db_name, + (const char *) event_name); + + invalid_creation_ctx= TRUE; + } + + if (load_collation(event_mem_root, + event_tbl->field[ET_FIELD_COLLATION_CONNECTION], + thd->variables.collation_connection, + &connection_cl)) + { + sql_print_warning("Event '%s'.'%s': invalid value " + "in column mysql.event.collation_connection.", + (const char *) db_name, + (const char *) event_name); + + invalid_creation_ctx= TRUE; + } + + if (load_collation(event_mem_root, + event_tbl->field[ET_FIELD_DB_COLLATION], + NULL, + &db_cl)) + { + sql_print_warning("Event '%s'.'%s': invalid value " + "in column mysql.event.db_collation.", + (const char *) db_name, + (const char *) event_name); + + invalid_creation_ctx= TRUE; + } + + /* + If we failed to resolve the database collation, load the default one + from the disk. + */ + + if (!db_cl) + db_cl= get_default_db_collation(thd, db_name); + + /* Create the context. */ + + *ctx= new Event_creation_ctx(client_cs, connection_cl, db_cl); + + return invalid_creation_ctx; +} + +/*************************************************************************/ + +/* + Initializes dbname and name of an Event_queue_element_for_exec + object + + SYNOPSIS + Event_queue_element_for_exec::init() + + RETURN VALUE + FALSE OK + TRUE Error (OOM) +*/ + +bool +Event_queue_element_for_exec::init(const LEX_CSTRING &db, const LEX_CSTRING &n) +{ + if (!(dbname.str= my_strndup(key_memory_Event_queue_element_for_exec_names, + db.str, dbname.length= db.length, MYF(MY_WME)))) + return TRUE; + if (!(name.str= my_strndup(key_memory_Event_queue_element_for_exec_names, + n.str, name.length= n.length, MYF(MY_WME)))) + { + my_free(const_cast<char*>(dbname.str)); + dbname.str= NULL; + return TRUE; + } + return FALSE; +} + + +/* + Destructor + + SYNOPSIS + Event_queue_element_for_exec::~Event_queue_element_for_exec() +*/ + +Event_queue_element_for_exec::~Event_queue_element_for_exec() +{ + my_free(const_cast<char*>(dbname.str)); + my_free(const_cast<char*>(name.str)); +} + + +/* + Constructor + + SYNOPSIS + Event_basic::Event_basic() +*/ + +Event_basic::Event_basic() +{ + DBUG_ENTER("Event_basic::Event_basic"); + /* init memory root */ + init_sql_alloc(key_memory_event_basic_root, &mem_root, 256, 512, MYF(0)); + dbname.str= name.str= NULL; + dbname.length= name.length= 0; + time_zone= NULL; + DBUG_VOID_RETURN; +} + + +/* + Destructor + + SYNOPSIS + Event_basic::Event_basic() +*/ + +Event_basic::~Event_basic() +{ + DBUG_ENTER("Event_basic::~Event_basic"); + free_root(&mem_root, MYF(0)); + DBUG_VOID_RETURN; +} + + +/* + Short function to load a char column into a LEX_CSTRING + + SYNOPSIS + Event_basic::load_string_field() + field_name The field( enum_events_table_field is not actually used + because it's unknown in event_data_objects.h) + fields The Field array + field_value The value +*/ + +bool +Event_basic::load_string_fields(Field **fields, ...) +{ + bool ret= FALSE; + va_list args; + enum enum_events_table_field field_name; + LEX_CSTRING *field_value; + + DBUG_ENTER("Event_basic::load_string_fields"); + + va_start(args, fields); + field_name= (enum enum_events_table_field) va_arg(args, int); + while (field_name < ET_FIELD_COUNT) + { + field_value= va_arg(args, LEX_CSTRING *); + if ((field_value->str= get_field(&mem_root, fields[field_name])) == NullS) + { + ret= TRUE; + break; + } + field_value->length= strlen(field_value->str); + + field_name= (enum enum_events_table_field) va_arg(args, int); + } + va_end(args); + + DBUG_RETURN(ret); +} + + +bool +Event_basic::load_time_zone(THD *thd, const LEX_CSTRING *tz_name) +{ + String str(tz_name->str, &my_charset_latin1); + time_zone= my_tz_find(thd, &str); + + return (time_zone == NULL); +} + + +/* + Constructor + + SYNOPSIS + Event_queue_element::Event_queue_element() +*/ + +Event_queue_element::Event_queue_element(): + on_completion(Event_parse_data::ON_COMPLETION_DROP), + status(Event_parse_data::ENABLED), expression(0), dropped(FALSE), + execution_count(0) +{ + DBUG_ENTER("Event_queue_element::Event_queue_element"); + + starts= ends= execute_at= last_executed= 0; + starts_null= ends_null= execute_at_null= TRUE; + + DBUG_VOID_RETURN; +} + + +/* + Destructor + + SYNOPSIS + Event_queue_element::Event_queue_element() +*/ +Event_queue_element::~Event_queue_element() +{ +} + + +/* + Constructor + + SYNOPSIS + Event_timed::Event_timed() +*/ + +Event_timed::Event_timed(): + created(0), modified(0), sql_mode(0) +{ + DBUG_ENTER("Event_timed::Event_timed"); + init(); + DBUG_VOID_RETURN; +} + + +/* + Destructor + + SYNOPSIS + Event_timed::~Event_timed() +*/ + +Event_timed::~Event_timed() +{ +} + + +/* + Constructor + + SYNOPSIS + Event_job_data::Event_job_data() +*/ + +Event_job_data::Event_job_data() + :sql_mode(0) +{ +} + +/* + Init all member variables + + SYNOPSIS + Event_timed::init() +*/ + +void +Event_timed::init() +{ + DBUG_ENTER("Event_timed::init"); + + definer_user.str= definer_host.str= body.str= comment.str= NULL; + definer_user.length= definer_host.length= body.length= comment.length= 0; + + sql_mode= 0; + + DBUG_VOID_RETURN; +} + + +/** + Load an event's body from a row from mysql.event. + + @details This method is silent on errors and should behave like that. + Callers should handle throwing of error messages. The reason is that the + class should not know about how to deal with communication. + + @return Operation status + @retval FALSE OK + @retval TRUE Error +*/ + +bool +Event_job_data::load_from_row(THD *thd, TABLE *table) +{ + const char *ptr; + size_t len; + LEX_CSTRING tz_name; + + DBUG_ENTER("Event_job_data::load_from_row"); + + if (!table) + DBUG_RETURN(TRUE); + + if (table->s->fields < ET_FIELD_COUNT) + DBUG_RETURN(TRUE); + + if (load_string_fields(table->field, + ET_FIELD_DB, &dbname, + ET_FIELD_NAME, &name, + ET_FIELD_BODY, &body, + ET_FIELD_DEFINER, &definer, + ET_FIELD_TIME_ZONE, &tz_name, + ET_FIELD_COUNT)) + DBUG_RETURN(TRUE); + + if (load_time_zone(thd, &tz_name)) + DBUG_RETURN(TRUE); + + Event_creation_ctx::load_from_db(thd, &mem_root, dbname.str, name.str, table, + &creation_ctx); + + ptr= strchr(definer.str, '@'); + + if (! ptr) + ptr= definer.str; + + len= ptr - definer.str; + definer_user.str= strmake_root(&mem_root, definer.str, len); + definer_user.length= len; + len= definer.length - len - 1; + /* 1:because of @ */ + definer_host.str= strmake_root(&mem_root, ptr + 1, len); + definer_host.length= len; + + sql_mode= (sql_mode_t) table->field[ET_FIELD_SQL_MODE]->val_int(); + + DBUG_RETURN(FALSE); +} + + +/** + Load an event's body from a row from mysql.event. + + @details This method is silent on errors and should behave like that. + Callers should handle throwing of error messages. The reason is that the + class should not know about how to deal with communication. + + @return Operation status + @retval FALSE OK + @retval TRUE Error +*/ + +bool +Event_queue_element::load_from_row(THD *thd, TABLE *table) +{ + const char *ptr; + MYSQL_TIME time; + LEX_CSTRING tz_name; + + DBUG_ENTER("Event_queue_element::load_from_row"); + + if (!table) + DBUG_RETURN(TRUE); + + if (table->s->fields < ET_FIELD_COUNT) + DBUG_RETURN(TRUE); + + if (load_string_fields(table->field, + ET_FIELD_DB, &dbname, + ET_FIELD_NAME, &name, + ET_FIELD_DEFINER, &definer, + ET_FIELD_TIME_ZONE, &tz_name, + ET_FIELD_COUNT)) + DBUG_RETURN(TRUE); + + if (load_time_zone(thd, &tz_name)) + DBUG_RETURN(TRUE); + + starts_null= table->field[ET_FIELD_STARTS]->is_null(); + uint not_used; + if (!starts_null) + { + /* + The expected data type for these columns in mysql.events: + starts, ends, execute_at, last_executed + is DATETIME. No nanosecond truncation should normally be needed, + unless the DBA changes them, e.g. to VARCHAR, DECIMAL, etc. + For this unexpected case let's use the default round mode, + according to the current session settings. + */ + table->field[ET_FIELD_STARTS]->get_date(&time, TIME_NO_ZERO_DATE | + thd->temporal_round_mode()); + starts= my_tz_OFFSET0->TIME_to_gmt_sec(&time,¬_used); + } + + ends_null= table->field[ET_FIELD_ENDS]->is_null(); + if (!ends_null) + { + table->field[ET_FIELD_ENDS]->get_date(&time, TIME_NO_ZERO_DATE | + thd->temporal_round_mode()); + ends= my_tz_OFFSET0->TIME_to_gmt_sec(&time,¬_used); + } + + if (!table->field[ET_FIELD_INTERVAL_EXPR]->is_null()) + expression= table->field[ET_FIELD_INTERVAL_EXPR]->val_int(); + else + expression= 0; + /* + If neigher STARTS and ENDS is set, then both fields are empty. + Hence, if ET_FIELD_EXECUTE_AT is empty there is an error. + */ + execute_at_null= table->field[ET_FIELD_EXECUTE_AT]->is_null(); + DBUG_ASSERT(!(starts_null && ends_null && !expression && execute_at_null)); + if (!expression && !execute_at_null) + { + if (table->field[ET_FIELD_EXECUTE_AT]->get_date(&time, TIME_NO_ZERO_DATE | + thd->temporal_round_mode())) + DBUG_RETURN(TRUE); + execute_at= my_tz_OFFSET0->TIME_to_gmt_sec(&time,¬_used); + } + + /* + We load the interval type from disk as string and then map it to + an integer. This decouples the values of enum interval_type + and values actually stored on disk. Therefore the type can be + reordered without risking incompatibilities of data between versions. + */ + if (!table->field[ET_FIELD_TRANSIENT_INTERVAL]->is_null()) + { + int i; + char buff[MAX_FIELD_WIDTH]; + String str(buff, sizeof(buff), &my_charset_bin); + LEX_CSTRING tmp; + + table->field[ET_FIELD_TRANSIENT_INTERVAL]->val_str(&str); + if (!(tmp.length= str.length())) + DBUG_RETURN(TRUE); + + tmp.str= str.c_ptr_safe(); + + i= find_string_in_array(interval_type_to_name, &tmp, system_charset_info); + if (i < 0) + DBUG_RETURN(TRUE); + interval= (interval_type) i; + } + + if (!table->field[ET_FIELD_LAST_EXECUTED]->is_null()) + { + table->field[ET_FIELD_LAST_EXECUTED]->get_date(&time, TIME_NO_ZERO_DATE | + thd->temporal_round_mode()); + last_executed= my_tz_OFFSET0->TIME_to_gmt_sec(&time,¬_used); + } + + if ((ptr= get_field(&mem_root, table->field[ET_FIELD_STATUS])) == NullS) + DBUG_RETURN(TRUE); + + DBUG_PRINT("load_from_row", ("Event [%s] is [%s]", name.str, ptr)); + + /* Set event status (ENABLED | SLAVESIDE_DISABLED | DISABLED) */ + switch (ptr[0]) + { + case 'E' : + status = Event_parse_data::ENABLED; + break; + case 'S' : + status = Event_parse_data::SLAVESIDE_DISABLED; + break; + case 'D' : + default: + status = Event_parse_data::DISABLED; + break; + } + if ((ptr= get_field(&mem_root, table->field[ET_FIELD_ORIGINATOR])) == NullS) + DBUG_RETURN(TRUE); + originator = (uint32) table->field[ET_FIELD_ORIGINATOR]->val_int(); + + /* ToDo : Andrey . Find a way not to allocate ptr on event_mem_root */ + if ((ptr= get_field(&mem_root, + table->field[ET_FIELD_ON_COMPLETION])) == NullS) + DBUG_RETURN(TRUE); + + on_completion= (ptr[0]=='D'? Event_parse_data::ON_COMPLETION_DROP: + Event_parse_data::ON_COMPLETION_PRESERVE); + + DBUG_RETURN(FALSE); +} + + +/** + Load an event's body from a row from mysql.event. + + @details This method is silent on errors and should behave like that. + Callers should handle throwing of error messages. The reason is that the + class should not know about how to deal with communication. + + @return Operation status + @retval FALSE OK + @retval TRUE Error +*/ + +bool +Event_timed::load_from_row(THD *thd, TABLE *table) +{ + const char *ptr; + size_t len; + + DBUG_ENTER("Event_timed::load_from_row"); + + if (Event_queue_element::load_from_row(thd, table)) + DBUG_RETURN(TRUE); + + if (load_string_fields(table->field, + ET_FIELD_BODY, &body, + ET_FIELD_BODY_UTF8, &body_utf8, + ET_FIELD_COUNT)) + DBUG_RETURN(TRUE); + + if (Event_creation_ctx::load_from_db(thd, &mem_root, dbname.str, name.str, + table, &creation_ctx)) + { + push_warning_printf(thd, + Sql_condition::WARN_LEVEL_WARN, + ER_EVENT_INVALID_CREATION_CTX, + ER_THD(thd, ER_EVENT_INVALID_CREATION_CTX), + (const char *) dbname.str, + (const char *) name.str); + } + + ptr= strchr(definer.str, '@'); + + if (! ptr) + ptr= definer.str; + + len= ptr - definer.str; + definer_user.str= strmake_root(&mem_root, definer.str, len); + definer_user.length= len; + len= definer.length - len - 1; + /* 1:because of @ */ + definer_host.str= strmake_root(&mem_root, ptr + 1, len); + definer_host.length= len; + + created= table->field[ET_FIELD_CREATED]->val_int(); + modified= table->field[ET_FIELD_MODIFIED]->val_int(); + + comment.str= get_field(&mem_root, table->field[ET_FIELD_COMMENT]); + if (comment.str != NullS) + comment.length= strlen(comment.str); + else + comment.length= 0; + + sql_mode= (sql_mode_t) table->field[ET_FIELD_SQL_MODE]->val_int(); + + DBUG_RETURN(FALSE); +} + + +/* + add_interval() adds a specified interval to time 'ltime' in time + zone 'time_zone', and returns the result converted to the number of + seconds since epoch (aka Unix time; in UTC time zone). Zero result + means an error. +*/ +static +my_time_t +add_interval(MYSQL_TIME *ltime, const Time_zone *time_zone, + interval_type scale, INTERVAL interval) +{ + if (date_add_interval(current_thd, ltime, scale, interval)) + return 0; + + uint not_used; + return time_zone->TIME_to_gmt_sec(ltime, ¬_used); +} + + +/* + Computes the sum of a timestamp plus interval. + + SYNOPSIS + get_next_time() + time_zone event time zone + next the sum + start add interval_value to this time + time_now current time + i_value quantity of time type interval to add + i_type type of interval to add (SECOND, MINUTE, HOUR, WEEK ...) + + RETURN VALUE + 0 OK + 1 Error + + NOTES + 1) If the interval is conversible to SECOND, like MINUTE, HOUR, DAY, WEEK. + Then we use TIMEDIFF()'s implementation as underlying and number of + seconds as resolution for computation. + 2) In all other cases - MONTH, QUARTER, YEAR we use MONTH as resolution + and PERIOD_DIFF()'s implementation +*/ + +static +bool get_next_time(const Time_zone *time_zone, my_time_t *next, + my_time_t start, my_time_t time_now, + int i_value, interval_type i_type) +{ + DBUG_ENTER("get_next_time"); + DBUG_PRINT("enter", ("start: %lu now: %lu", (long) start, (long) time_now)); + + DBUG_ASSERT(start <= time_now); + + longlong months=0, seconds=0; + + switch (i_type) { + case INTERVAL_YEAR: + months= i_value*12; + break; + case INTERVAL_QUARTER: + /* Has already been converted to months */ + case INTERVAL_YEAR_MONTH: + case INTERVAL_MONTH: + months= i_value; + break; + case INTERVAL_WEEK: + /* WEEK has already been converted to days */ + case INTERVAL_DAY: + seconds= i_value*24*3600; + break; + case INTERVAL_DAY_HOUR: + case INTERVAL_HOUR: + seconds= i_value*3600; + break; + case INTERVAL_DAY_MINUTE: + case INTERVAL_HOUR_MINUTE: + case INTERVAL_MINUTE: + seconds= i_value*60; + break; + case INTERVAL_DAY_SECOND: + case INTERVAL_HOUR_SECOND: + case INTERVAL_MINUTE_SECOND: + case INTERVAL_SECOND: + seconds= i_value; + break; + case INTERVAL_DAY_MICROSECOND: + case INTERVAL_HOUR_MICROSECOND: + case INTERVAL_MINUTE_MICROSECOND: + case INTERVAL_SECOND_MICROSECOND: + case INTERVAL_MICROSECOND: + /* + We should return an error here so SHOW EVENTS/ SELECT FROM I_S.EVENTS + would give an error then. + */ + DBUG_RETURN(1); + case INTERVAL_LAST: + DBUG_ASSERT(0); + } + DBUG_PRINT("info", ("seconds: %ld months: %ld", (long) seconds, (long) months)); + + MYSQL_TIME local_start; + MYSQL_TIME local_now; + + /* Convert times from UTC to local. */ + { + time_zone->gmt_sec_to_TIME(&local_start, start); + time_zone->gmt_sec_to_TIME(&local_now, time_now); + } + + INTERVAL interval; + bzero(&interval, sizeof(interval)); + my_time_t next_time= 0; + + if (seconds) + { + ulonglong seconds_diff; + ulong microsec_diff; + bool negative= calc_time_diff(&local_now, &local_start, 1, + &seconds_diff, µsec_diff); + if (!negative) + { + /* + The formula below returns the interval that, when added to + local_start, will always give the time in the future. + */ + interval.second= seconds_diff - seconds_diff % seconds + seconds; + next_time= add_interval(&local_start, time_zone, + INTERVAL_SECOND, interval); + if (next_time == 0) + goto done; + } + + if (next_time <= time_now) + { + /* + If 'negative' is true above, then 'next_time == 0', and + 'next_time <= time_now' is also true. If negative is false, + then next_time was set, but perhaps to the value that is less + then time_now. See below for elaboration. + */ + DBUG_ASSERT(negative || next_time > 0); + + /* + If local_now < local_start, i.e. STARTS time is in the future + according to the local time (it always in the past according + to UTC---this is a prerequisite of this function), then + STARTS is almost always in the past according to the local + time too. However, in the time zone that has backward + Daylight Saving Time shift, the following may happen: suppose + we have a backward DST shift at certain date after 2:59:59, + i.e. local time goes 1:59:59, 2:00:00, ... , 2:59:59, (shift + here) 2:00:00 (again), ... , 2:59:59 (again), 3:00:00, ... . + Now suppose the time has passed the first 2:59:59, has been + shifted backward, and now is (the second) 2:20:00. The user + does CREATE EVENT with STARTS 'current-date 2:40:00'. Local + time 2:40:00 from create statement is treated by time + functions as the first such time, so according to UTC it comes + before the second 2:20:00. But according to local time it is + obviously in the future, so we end up in this branch. + + Since we are in the second pass through 2:00:00--2:59:59, and + any local time form this interval is treated by system + functions as the time from the first pass, we have to find the + time for the next execution that is past the DST-affected + interval (past the second 2:59:59 for our example, + i.e. starting from 3:00:00). We do this in the loop until the + local time is mapped onto future UTC time. 'start' time is in + the past, so we may use 'do { } while' here, and add the first + interval right away. + + Alternatively, it could be that local_now >= local_start. Now + for the example above imagine we do CREATE EVENT with STARTS + 'current-date 2:10:00'. Local start 2:10 is in the past (now + is local 2:20), so we add an interval, and get next execution + time, say, 2:40. It is in the future according to local time, + but, again, since we are in the second pass through + 2:00:00--2:59:59, 2:40 will be converted into UTC time in the + past. So we will end up in this branch again, and may add + intervals in a 'do { } while' loop. + + Note that for any given event we may end up here only if event + next execution time will map to the time interval that is + passed twice, and only if the server was started during the + second pass, or the event is being created during the second + pass. After that, we never will get here (unless we again + start the server during the second pass). In other words, + such a condition is extremely rare. + */ + interval.second= seconds; + do + { + next_time= add_interval(&local_start, time_zone, + INTERVAL_SECOND, interval); + if (next_time == 0) + goto done; + } + while (next_time <= time_now); + } + } + else + { + long diff_months= ((long) local_now.year - (long) local_start.year)*12 + + ((long) local_now.month - (long) local_start.month); + + /* + Unlike for seconds above, the formula below returns the interval + that, when added to the local_start, will give the time in the + past, or somewhere in the current month. We are interested in + the latter case, to see if this time has already passed, or is + yet to come this month. + + Note that the time is guaranteed to be in the past unless + (diff_months % months == 0), but no good optimization is + possible here, because (diff_months % months == 0) is what will + happen most of the time, as get_next_time() will be called right + after the execution of the event. We could pass last_executed + time to this function, and see if the execution has already + happened this month, but for that we will have to convert + last_executed from seconds since epoch to local broken-down + time, and this will greatly reduce the effect of the + optimization. So instead we keep the code simple and clean. + */ + interval.month= (ulong) (diff_months - diff_months % months); + next_time= add_interval(&local_start, time_zone, + INTERVAL_MONTH, interval); + if (next_time == 0) + goto done; + + if (next_time <= time_now) + { + interval.month= (ulong) months; + next_time= add_interval(&local_start, time_zone, + INTERVAL_MONTH, interval); + if (next_time == 0) + goto done; + } + } + + DBUG_ASSERT(time_now < next_time); + + *next= next_time; + +done: + DBUG_PRINT("info", ("next_time: %ld", (long) next_time)); + DBUG_RETURN(next_time == 0); +} + + +/* + Computes next execution time. + + SYNOPSIS + Event_queue_element::compute_next_execution_time() + + RETURN VALUE + FALSE OK + TRUE Error + + NOTES + The time is set in execute_at, if no more executions the latter is + set to 0. +*/ + +bool +Event_queue_element::compute_next_execution_time() +{ + my_time_t time_now; + DBUG_ENTER("Event_queue_element::compute_next_execution_time"); + DBUG_PRINT("enter", ("starts: %lu ends: %lu last_executed: %lu this: %p", + (long) starts, (long) ends, (long) last_executed, + this)); + + if (status != Event_parse_data::ENABLED) + { + DBUG_PRINT("compute_next_execution_time", + ("Event %s is DISABLED", name.str)); + goto ret; + } + /* If one-time, no need to do computation */ + if (!expression) + { + /* Let's check whether it was executed */ + if (last_executed) + { + DBUG_PRINT("info",("One-time event %s.%s of was already executed", + dbname.str, name.str)); + dropped= (on_completion == Event_parse_data::ON_COMPLETION_DROP); + DBUG_PRINT("info",("One-time event will be dropped: %d.", dropped)); + + status= Event_parse_data::DISABLED; + } + goto ret; + } + + time_now= current_thd->query_start(); + + DBUG_PRINT("info",("NOW: [%lu]", (ulong) time_now)); + + /* if time_now is after ends don't execute anymore */ + if (!ends_null && ends < time_now) + { + DBUG_PRINT("info", ("NOW after ENDS, don't execute anymore")); + /* time_now is after ends. don't execute anymore */ + execute_at= 0; + execute_at_null= TRUE; + if (on_completion == Event_parse_data::ON_COMPLETION_DROP) + dropped= TRUE; + DBUG_PRINT("info", ("Dropped: %d", dropped)); + status= Event_parse_data::DISABLED; + + goto ret; + } + + /* + Here time_now is before or equals ends if the latter is set. + Let's check whether time_now is before starts. + If so schedule for starts. + */ + if (!starts_null && time_now <= starts) + { + if (time_now == starts && starts == last_executed) + { + /* + do nothing or we will schedule for second time execution at starts. + */ + } + else + { + DBUG_PRINT("info", ("STARTS is future, NOW <= STARTS,sched for STARTS")); + /* + starts is in the future + time_now before starts. Scheduling for starts + */ + execute_at= starts; + execute_at_null= FALSE; + goto ret; + } + } + + if (!starts_null && !ends_null) + { + /* + Both starts and m_ends are set and time_now is between them (incl.) + If last_executed is set then increase with m_expression. The new MYSQL_TIME is + after m_ends set execute_at to 0. And check for on_completion + If not set then schedule for now. + */ + DBUG_PRINT("info", ("Both STARTS & ENDS are set")); + if (!last_executed) + { + DBUG_PRINT("info", ("Not executed so far.")); + } + + { + my_time_t next_exec; + + if (get_next_time(time_zone, &next_exec, starts, time_now, + (int) expression, interval)) + goto err; + + /* There was previous execution */ + if (ends < next_exec) + { + DBUG_PRINT("info", ("Next execution of %s after ENDS. Stop executing.", + name.str)); + /* Next execution after ends. No more executions */ + execute_at= 0; + execute_at_null= TRUE; + if (on_completion == Event_parse_data::ON_COMPLETION_DROP) + dropped= TRUE; + status= Event_parse_data::DISABLED; + } + else + { + DBUG_PRINT("info",("Next[%lu]", (ulong) next_exec)); + execute_at= next_exec; + execute_at_null= FALSE; + } + } + goto ret; + } + else if (starts_null && ends_null) + { + /* starts is always set, so this is a dead branch !! */ + DBUG_PRINT("info", ("Neither STARTS nor ENDS are set")); + /* + Both starts and m_ends are not set, so we schedule for the next + based on last_executed. + */ + if (last_executed) + { + my_time_t next_exec; + if (get_next_time(time_zone, &next_exec, starts, time_now, + (int) expression, interval)) + goto err; + execute_at= next_exec; + DBUG_PRINT("info",("Next[%lu]", (ulong) next_exec)); + } + else + { + /* last_executed not set. Schedule the event for now */ + DBUG_PRINT("info", ("Execute NOW")); + execute_at= time_now; + } + execute_at_null= FALSE; + } + else + { + /* either starts or m_ends is set */ + if (!starts_null) + { + DBUG_PRINT("info", ("STARTS is set")); + /* + - starts is set. + - starts is not in the future according to check made before + Hence schedule for starts + m_expression in case last_executed + is not set, otherwise to last_executed + m_expression + */ + if (!last_executed) + { + DBUG_PRINT("info", ("Not executed so far.")); + } + + { + my_time_t next_exec; + if (get_next_time(time_zone, &next_exec, starts, time_now, + (int) expression, interval)) + goto err; + execute_at= next_exec; + DBUG_PRINT("info",("Next[%lu]", (ulong) next_exec)); + } + execute_at_null= FALSE; + } + else + { + /* this is a dead branch, because starts is always set !!! */ + DBUG_PRINT("info", ("STARTS is not set. ENDS is set")); + /* + - m_ends is set + - m_ends is after time_now or is equal + Hence check for m_last_execute and increment with m_expression. + If last_executed is not set then schedule for now + */ + + if (!last_executed) + execute_at= time_now; + else + { + my_time_t next_exec; + + if (get_next_time(time_zone, &next_exec, starts, time_now, + (int) expression, interval)) + goto err; + + if (ends < next_exec) + { + DBUG_PRINT("info", ("Next execution after ENDS. Stop executing.")); + execute_at= 0; + execute_at_null= TRUE; + status= Event_parse_data::DISABLED; + if (on_completion == Event_parse_data::ON_COMPLETION_DROP) + dropped= TRUE; + } + else + { + DBUG_PRINT("info", ("Next[%lu]", (ulong) next_exec)); + execute_at= next_exec; + execute_at_null= FALSE; + } + } + } + goto ret; + } +ret: + DBUG_PRINT("info", ("ret: 0 execute_at: %lu", (long) execute_at)); + DBUG_RETURN(FALSE); +err: + DBUG_PRINT("info", ("ret=1")); + DBUG_RETURN(TRUE); +} + + +/* + Set the internal last_executed MYSQL_TIME struct to now. NOW is the + time according to thd->query_start(), so the THD's clock. + + SYNOPSIS + Event_queue_element::mark_last_executed() + thd thread context +*/ + +void +Event_queue_element::mark_last_executed(THD *thd) +{ + last_executed= thd->query_start(); + + execution_count++; +} + + +static +void +append_datetime(String *buf, Time_zone *time_zone, my_time_t secs, + const char *name, uint len) +{ + char dtime_buff[20*2+32];/* +32 to make my_snprintf_{8bit|ucs2} happy */ + buf->append(STRING_WITH_LEN(" ")); + buf->append(name, len); + buf->append(STRING_WITH_LEN(" '")); + /* + Pass the buffer and the second param tells fills the buffer and + returns the number of chars to copy. + */ + MYSQL_TIME time; + time_zone->gmt_sec_to_TIME(&time, secs); + buf->append(dtime_buff, my_datetime_to_str(&time, dtime_buff, 0)); + buf->append(STRING_WITH_LEN("'")); +} + + +/* + Get SHOW CREATE EVENT as string + + SYNOPSIS + Event_timed::get_create_event(THD *thd, String *buf) + thd Thread + buf String*, should be already allocated. CREATE EVENT goes inside. + + RETURN VALUE + 0 OK + EVEX_MICROSECOND_UNSUP Error (for now if mysql.event has been + tampered and MICROSECONDS interval or + derivative has been put there. +*/ + +int +Event_timed::get_create_event(THD *thd, String *buf) +{ + char tmp_buf[2 * STRING_BUFFER_USUAL_SIZE]; + String expr_buf(tmp_buf, sizeof(tmp_buf), system_charset_info); + expr_buf.length(0); + + DBUG_ENTER("get_create_event"); + DBUG_PRINT("ret_info",("body_len=[%d]body=[%s]", + (int) body.length, body.str)); + + if (expression && Events::reconstruct_interval_expression(&expr_buf, interval, + expression)) + DBUG_RETURN(EVEX_MICROSECOND_UNSUP); + + buf->append(STRING_WITH_LEN("CREATE ")); + append_definer(thd, buf, &definer_user, &definer_host); + buf->append(STRING_WITH_LEN("EVENT ")); + append_identifier(thd, buf, &name); + + if (expression) + { + buf->append(STRING_WITH_LEN(" ON SCHEDULE EVERY ")); + buf->append(expr_buf); + buf->append(' '); + LEX_CSTRING *ival= &interval_type_to_name[interval]; + buf->append(ival->str, ival->length); + + if (!starts_null) + append_datetime(buf, time_zone, starts, STRING_WITH_LEN("STARTS")); + + if (!ends_null) + append_datetime(buf, time_zone, ends, STRING_WITH_LEN("ENDS")); + } + else + { + append_datetime(buf, time_zone, execute_at, + STRING_WITH_LEN("ON SCHEDULE AT")); + } + + if (on_completion == Event_parse_data::ON_COMPLETION_DROP) + buf->append(STRING_WITH_LEN(" ON COMPLETION NOT PRESERVE ")); + else + buf->append(STRING_WITH_LEN(" ON COMPLETION PRESERVE ")); + + if (status == Event_parse_data::ENABLED) + buf->append(STRING_WITH_LEN("ENABLE")); + else if (status == Event_parse_data::SLAVESIDE_DISABLED) + buf->append(STRING_WITH_LEN("DISABLE ON SLAVE")); + else + buf->append(STRING_WITH_LEN("DISABLE")); + + if (comment.length) + { + buf->append(STRING_WITH_LEN(" COMMENT ")); + append_unescaped(buf, comment.str, comment.length); + } + buf->append(STRING_WITH_LEN(" DO ")); + buf->append(&body); + + DBUG_RETURN(0); +} + + +/** + Get an artificial stored procedure to parse as an event definition. +*/ + +bool +Event_job_data::construct_sp_sql(THD *thd, String *sp_sql) +{ + LEX_CSTRING buffer; + const uint STATIC_SQL_LENGTH= 44; + + DBUG_ENTER("Event_job_data::construct_sp_sql"); + + /* + Allocate a large enough buffer on the thread execution memory + root to avoid multiple [re]allocations on system heap + */ + buffer.length= STATIC_SQL_LENGTH + name.length + body.length; + if (! (buffer.str= (char*) thd->alloc(buffer.length))) + DBUG_RETURN(TRUE); + + sp_sql->set(buffer.str, buffer.length, system_charset_info); + sp_sql->length(0); + + + sp_sql->append(STRING_WITH_LEN("CREATE ")); + sp_sql->append(STRING_WITH_LEN("PROCEDURE ")); + /* + Let's use the same name as the event name to perhaps produce a + better error message in case it is a part of some parse error. + We're using append_identifier here to successfully parse + events with reserved names. + */ + append_identifier(thd, sp_sql, &name); + + /* + The default SQL security of a stored procedure is DEFINER. We + have already activated the security context of the event, so + let's execute the procedure with the invoker rights to save on + resets of security contexts. + */ + sp_sql->append(STRING_WITH_LEN("() SQL SECURITY INVOKER ")); + + if (thd->variables.sql_mode & MODE_ORACLE) + sp_sql->append(STRING_WITH_LEN(" AS BEGIN ")); + sp_sql->append(&body); + if (thd->variables.sql_mode & MODE_ORACLE) + sp_sql->append(STRING_WITH_LEN("; END")); + + DBUG_RETURN(thd->is_fatal_error); +} + + +/** + Get DROP EVENT statement to binlog the drop of ON COMPLETION NOT + PRESERVE event. +*/ + +bool +Event_job_data::construct_drop_event_sql(THD *thd, String *sp_sql) +{ + LEX_CSTRING buffer; + const uint STATIC_SQL_LENGTH= 14; + + DBUG_ENTER("Event_job_data::construct_drop_event_sql"); + + buffer.length= STATIC_SQL_LENGTH + name.length*2 + dbname.length*2; + if (! (buffer.str= (char*) thd->alloc(buffer.length))) + DBUG_RETURN(TRUE); + + sp_sql->set(buffer.str, buffer.length, system_charset_info); + sp_sql->length(0); + + sp_sql->append(STRING_WITH_LEN("DROP EVENT ")); + append_identifier(thd, sp_sql, &dbname); + sp_sql->append('.'); + append_identifier(thd, sp_sql, &name); + + DBUG_RETURN(thd->is_fatal_error); +} + +/** + Compiles and executes the event (the underlying sp_head object) + + @retval TRUE error (reported to the error log) + @retval FALSE success +*/ + +bool +Event_job_data::execute(THD *thd, bool drop) +{ + String sp_sql; +#ifndef NO_EMBEDDED_ACCESS_CHECKS + Security_context event_sctx, *save_sctx= NULL; +#endif + List<Item> empty_item_list; + bool ret= TRUE; + + DBUG_ENTER("Event_job_data::execute"); + + thd->reset_for_next_command(); + +#ifdef WITH_WSREP + wsrep_open(thd); + wsrep_before_command(thd); +#endif /* WITH_WSREP */ + /* + MySQL parser currently assumes that current database is either + present in THD or all names in all statements are fully specified. + And yet not fully specified names inside stored programs must be + be supported, even if the current database is not set: + CREATE PROCEDURE db1.p1() BEGIN CREATE TABLE t1; END// + -- in this example t1 should be always created in db1 and the statement + must parse even if there is no current database. + + To support this feature and still address the parser limitation, + we need to set the current database here. + We don't have to call mysql_change_db, since the checks performed + in it are unnecessary for the purpose of parsing, and + mysql_change_db will be invoked anyway later, to activate the + procedure database before it's executed. + */ + thd->set_db(&dbname); + + lex_start(thd); + +#ifndef NO_EMBEDDED_ACCESS_CHECKS + if (event_sctx.change_security_context(thd, + &definer_user, &definer_host, + &dbname, &save_sctx)) + { + sql_print_error("Event Scheduler: " + "[%s].[%s.%s] execution failed, " + "failed to authenticate the user.", + definer.str, dbname.str, name.str); + goto end; + } +#endif + + if (check_access(thd, EVENT_ACL, dbname.str, NULL, NULL, 0, 0)) + { + /* + This aspect of behavior is defined in the worklog, + and this is how triggers work too: if TRIGGER + privilege is revoked from trigger definer, + triggers are not executed. + */ + sql_print_error("Event Scheduler: " + "[%s].[%s.%s] execution failed, " + "user no longer has EVENT privilege.", + definer.str, dbname.str, name.str); + goto end; + } + + /* + Set up global thread attributes to reflect the properties of + this Event. We can simply reset these instead of usual + backup/restore employed in stored programs since we know that + this is a top level statement and the worker thread is + allocated exclusively to execute this event. + */ + + thd->variables.sql_mode= sql_mode; + thd->variables.time_zone= time_zone; + + if (construct_sp_sql(thd, &sp_sql)) + goto end; + + thd->set_query(sp_sql.c_ptr_safe(), sp_sql.length()); + + { + Parser_state parser_state; + sql_digest_state *parent_digest= thd->m_digest; + PSI_statement_locker *parent_locker= thd->m_statement_psi; + bool res; + + if (parser_state.init(thd, thd->query(), thd->query_length())) + goto end; + + thd->m_digest= NULL; + thd->m_statement_psi= NULL; + res= parse_sql(thd, & parser_state, creation_ctx); + thd->m_digest= parent_digest; + thd->m_statement_psi= parent_locker; + + if (res) + { + sql_print_error("Event Scheduler: %serror during compilation of %s.%s", + thd->is_fatal_error ? "fatal " : "", dbname.str, name.str); + goto end; + } + } + + { + sp_head *sphead= thd->lex->sphead; + + DBUG_ASSERT(sphead); + + sphead->m_flags|= sp_head::LOG_SLOW_STATEMENTS; + sphead->m_flags|= sp_head::LOG_GENERAL_LOG; + + /* + construct_sp_sql() + parse_sql() set suid to SP_IS_NOT_SUID, + because we have the security context already set to the event + definer here. See more comments in construct_sp_sql(). + */ + DBUG_ASSERT(sphead->suid() == SP_IS_NOT_SUID); + sphead->m_sql_mode= sql_mode; + sphead->set_creation_ctx(creation_ctx); + sphead->optimize(); + + sphead->m_sp_share= MYSQL_GET_SP_SHARE(SP_TYPE_EVENT, + dbname.str, static_cast<uint>(dbname.length), + name.str, static_cast<uint>(name.length)); + ret= sphead->execute_procedure(thd, &empty_item_list); + /* + There is no pre-locking and therefore there should be no + tables open and locked left after execute_procedure. + */ + } + +end: + if (drop && likely(!thd->is_fatal_error)) + { + /* + We must do it here since here we're under the right authentication + ID of the event definer. + */ + sql_print_information("Event Scheduler: Dropping %s.%s", + (const char *) dbname.str, (const char *) name.str); + /* + Construct a query for the binary log, to ensure the event is dropped + on the slave + */ + if (construct_drop_event_sql(thd, &sp_sql)) + ret= 1; + else + { + thd->set_query(sp_sql.c_ptr_safe(), sp_sql.length()); + + /* + NOTE: even if we run in read-only mode, we should be able to lock + the mysql.event table for writing. In order to achieve this, we + should call mysql_lock_tables() under the super-user. + + Same goes for transaction access mode. + Temporarily reset it to read-write. + */ + + privilege_t saved_master_access(thd->security_ctx->master_access); + thd->security_ctx->master_access |= PRIV_IGNORE_READ_ONLY; + bool save_tx_read_only= thd->tx_read_only; + thd->tx_read_only= false; + + /* + This code is processing event execution and does not have client + connection. Here, event execution will now execute a prepared + DROP EVENT statement, but thd->lex->sql_command is set to + SQLCOM_CREATE_PROCEDURE + DROP EVENT will be logged in binlog, and we have to + replicate it to make all nodes have consistent event definitions + Wsrep DDL replication is triggered inside Events::drop_event(), + and here we need to prepare the THD so that DDL replication is + possible, essentially it requires setting sql_command to + SQLCOMM_DROP_EVENT, we will switch sql_command for the duration + of DDL replication only. + */ + const enum_sql_command sql_command_save= thd->lex->sql_command; + const bool sql_command_set= WSREP(thd); + + if (sql_command_set) + thd->lex->sql_command = SQLCOM_DROP_EVENT; + + ret= Events::drop_event(thd, &dbname, &name, FALSE); + + if (sql_command_set) + { + WSREP_TO_ISOLATION_END; + thd->lex->sql_command = sql_command_save; + } + + thd->tx_read_only= save_tx_read_only; + thd->security_ctx->master_access= saved_master_access; + } + } +#ifndef NO_EMBEDDED_ACCESS_CHECKS + if (save_sctx) + event_sctx.restore_security_context(thd, save_sctx); +#endif +#ifdef WITH_WSREP + wsrep_after_command_ignore_result(thd); + wsrep_close(thd); +#endif /* WITH_WSREP */ + thd->lex->unit.cleanup(); + thd->end_statement(); + thd->cleanup_after_query(); + /* Avoid races with SHOW PROCESSLIST */ + thd->reset_query(); + + DBUG_PRINT("info", ("EXECUTED %s.%s ret: %d", dbname.str, name.str, ret)); + + DBUG_RETURN(ret); +} + + +/* + Checks whether two events are in the same schema + + SYNOPSIS + event_basic_db_equal() + db Schema + et Compare et->dbname to `db` + + RETURN VALUE + TRUE Equal + FALSE Not equal +*/ + +bool +event_basic_db_equal(const LEX_CSTRING *db, Event_basic *et) +{ + return !sortcmp_lex_string(&et->dbname, db, system_charset_info); +} + + +/* + Checks whether an event has equal `db` and `name` + + SYNOPSIS + event_basic_identifier_equal() + db Schema + name Name + et The event object + + RETURN VALUE + TRUE Equal + FALSE Not equal +*/ + +bool +event_basic_identifier_equal(const LEX_CSTRING *db, const LEX_CSTRING *name, + Event_basic *b) +{ + return !sortcmp_lex_string(name, &b->name, system_charset_info) && + !sortcmp_lex_string(db, &b->dbname, system_charset_info); +} + +/** + @} (End of group Event_Scheduler) +*/ |