forked from StoneAtom/StoneDB
499 lines
15 KiB
C++
499 lines
15 KiB
C++
/*
|
|
Copyright (c) 2004, 2021, 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, version 2.0,
|
|
as published by the Free Software Foundation.
|
|
|
|
This program is also distributed with certain software (including
|
|
but not limited to OpenSSL) that is licensed under separate terms,
|
|
as designated in a particular file or component or in included license
|
|
documentation. The authors of MySQL hereby grant you an additional
|
|
permission to link the program and your derivative works with the
|
|
separately licensed software that they have included with MySQL.
|
|
|
|
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, version 2.0, for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */
|
|
|
|
|
|
#include "my_global.h" // NO_EMBEDDED_ACCESS_CHECKS
|
|
#include "sql_trigger.h"
|
|
|
|
#include "auth_common.h" // check_table_access
|
|
#include "sp.h" // sp_add_to_query_tables()
|
|
#include "sql_base.h" // find_temporary_table()
|
|
#include "sql_table.h" // build_table_filename()
|
|
// write_bin_log()
|
|
#include "sql_handler.h" // mysql_ha_rm_tables()
|
|
#include "sp_cache.h" // sp_invalidate_cache()
|
|
#include "trigger_loader.h" // Trigger_loader
|
|
#include "table_trigger_dispatcher.h" // Table_trigger_dispatcher
|
|
#include "binlog.h"
|
|
#include "sp_head.h" // sp_name
|
|
|
|
#include "mysql/psi/mysql_sp.h"
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
Create or drop trigger for table.
|
|
|
|
@param thd current thread context (including trigger definition in LEX)
|
|
@param tables table list containing one table for which trigger is created.
|
|
@param create whenever we create (TRUE) or drop (FALSE) trigger
|
|
|
|
@note
|
|
This function is mainly responsible for opening and locking of table and
|
|
invalidation of all its instances in table cache after trigger creation.
|
|
Real work on trigger creation/dropping is done inside
|
|
Table_trigger_dispatcher methods.
|
|
|
|
@todo
|
|
TODO: We should check if user has TRIGGER privilege for table here.
|
|
Now we just require SUPER privilege for creating/dropping because
|
|
we don't have proper privilege checking for triggers in place yet.
|
|
|
|
@retval
|
|
FALSE Success
|
|
@retval
|
|
TRUE error
|
|
*/
|
|
bool mysql_create_or_drop_trigger(THD *thd, TABLE_LIST *tables, bool create)
|
|
{
|
|
/*
|
|
FIXME: The code below takes too many different paths depending on the
|
|
'create' flag, so that the justification for a single function
|
|
'mysql_create_or_drop_trigger', compared to two separate functions
|
|
'mysql_create_trigger' and 'mysql_drop_trigger' is not apparent.
|
|
This is a good candidate for a minor refactoring.
|
|
*/
|
|
TABLE *table;
|
|
bool result= TRUE;
|
|
String stmt_query;
|
|
bool lock_upgrade_done= FALSE;
|
|
MDL_ticket *mdl_ticket= NULL;
|
|
Query_tables_list backup;
|
|
|
|
DBUG_ENTER("mysql_create_or_drop_trigger");
|
|
|
|
/* Charset of the buffer for statement must be system one. */
|
|
stmt_query.set_charset(system_charset_info);
|
|
|
|
/*
|
|
QQ: This function could be merged in mysql_alter_table() function
|
|
But do we want this ?
|
|
*/
|
|
|
|
/*
|
|
Note that once we will have check for TRIGGER privilege in place we won't
|
|
need second part of condition below, since check_access() function also
|
|
checks that db is specified.
|
|
*/
|
|
if (!thd->lex->spname->m_db.length || (create && !tables->db_length))
|
|
{
|
|
my_error(ER_NO_DB_ERROR, MYF(0));
|
|
DBUG_RETURN(TRUE);
|
|
}
|
|
|
|
/*
|
|
We don't allow creating triggers on tables in the 'mysql' schema
|
|
*/
|
|
if (create && !my_strcasecmp(system_charset_info, "mysql", tables->db))
|
|
{
|
|
my_error(ER_NO_TRIGGERS_ON_SYSTEM_SCHEMA, MYF(0));
|
|
DBUG_RETURN(TRUE);
|
|
}
|
|
|
|
/*
|
|
There is no DETERMINISTIC clause for triggers, so can't check it.
|
|
But a trigger can in theory be used to do nasty things (if it supported
|
|
DROP for example) so we do the check for privileges. For now there is
|
|
already a stronger test right above; but when this stronger test will
|
|
be removed, the test below will hold. Because triggers have the same
|
|
nature as functions regarding binlogging: their body is implicitly
|
|
binlogged, so they share the same danger, so trust_function_creators
|
|
applies to them too.
|
|
*/
|
|
if (!trust_function_creators && mysql_bin_log.is_open() &&
|
|
!(thd->security_context()->check_access(SUPER_ACL)))
|
|
{
|
|
my_error(ER_BINLOG_CREATE_ROUTINE_NEED_SUPER, MYF(0));
|
|
DBUG_RETURN(TRUE);
|
|
}
|
|
|
|
if (!create)
|
|
{
|
|
bool if_exists= thd->lex->drop_if_exists;
|
|
|
|
/*
|
|
Protect the query table list from the temporary and potentially
|
|
destructive changes necessary to open the trigger's table.
|
|
*/
|
|
thd->lex->reset_n_backup_query_tables_list(&backup);
|
|
/*
|
|
Restore Query_tables_list::sql_command, which was
|
|
reset above, as the code that writes the query to the
|
|
binary log assumes that this value corresponds to the
|
|
statement that is being executed.
|
|
*/
|
|
thd->lex->sql_command= backup.sql_command;
|
|
|
|
if (check_readonly(thd, true))
|
|
goto end;
|
|
|
|
if (add_table_for_trigger(thd,
|
|
thd->lex->spname->m_db,
|
|
thd->lex->spname->m_name,
|
|
if_exists, & tables))
|
|
goto end;
|
|
|
|
if (!tables)
|
|
{
|
|
assert(if_exists);
|
|
/*
|
|
Since the trigger does not exist, there is no associated table,
|
|
and therefore :
|
|
- no TRIGGER privileges to check,
|
|
- no trigger to drop,
|
|
- no table to lock/modify,
|
|
so the drop statement is successful.
|
|
*/
|
|
result= FALSE;
|
|
/* Still, we need to log the query ... */
|
|
stmt_query.append(thd->query().str, thd->query().length);
|
|
goto end;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Check that the user has TRIGGER privilege on the subject table.
|
|
*/
|
|
{
|
|
bool err_status;
|
|
TABLE_LIST **save_query_tables_own_last= thd->lex->query_tables_own_last;
|
|
thd->lex->query_tables_own_last= 0;
|
|
|
|
err_status= check_table_access(thd, TRIGGER_ACL, tables, FALSE, 1, FALSE);
|
|
|
|
thd->lex->query_tables_own_last= save_query_tables_own_last;
|
|
|
|
if (err_status)
|
|
goto end;
|
|
}
|
|
|
|
/* We should have only one table in table list. */
|
|
assert(tables->next_global == 0);
|
|
|
|
/* We do not allow creation of triggers on temporary tables. */
|
|
if (create && find_temporary_table(thd, tables))
|
|
{
|
|
my_error(ER_TRG_ON_VIEW_OR_TEMP_TABLE, MYF(0), tables->alias);
|
|
goto end;
|
|
}
|
|
|
|
/* We also don't allow creation of triggers on views. */
|
|
tables->required_type= FRMTYPE_TABLE;
|
|
/*
|
|
Also prevent DROP TRIGGER from opening temporary table which might
|
|
shadow the subject table on which trigger to be dropped is defined.
|
|
*/
|
|
tables->open_type= OT_BASE_ONLY;
|
|
|
|
/* Keep consistent with respect to other DDL statements */
|
|
mysql_ha_rm_tables(thd, tables);
|
|
|
|
if (thd->locked_tables_mode)
|
|
{
|
|
/* Under LOCK TABLES we must only accept write locked tables. */
|
|
if (!(tables->table= find_table_for_mdl_upgrade(thd, tables->db,
|
|
tables->table_name,
|
|
FALSE)))
|
|
goto end;
|
|
}
|
|
else
|
|
{
|
|
tables->table= open_n_lock_single_table(thd, tables,
|
|
TL_READ_NO_INSERT, 0);
|
|
if (! tables->table)
|
|
goto end;
|
|
tables->table->use_all_columns();
|
|
}
|
|
table= tables->table;
|
|
table->pos_in_table_list= tables;
|
|
|
|
/* Later on we will need it to downgrade the lock */
|
|
mdl_ticket= table->mdl_ticket;
|
|
|
|
if (wait_while_table_is_used(thd, table, HA_EXTRA_FORCE_REOPEN))
|
|
goto end;
|
|
|
|
lock_upgrade_done= TRUE;
|
|
|
|
if (!table->triggers)
|
|
{
|
|
if (!create)
|
|
{
|
|
my_error(ER_TRG_DOES_NOT_EXIST, MYF(0));
|
|
goto end;
|
|
}
|
|
|
|
if (!(table->triggers= Table_trigger_dispatcher::create(table)))
|
|
goto end;
|
|
}
|
|
|
|
if (create)
|
|
{
|
|
result= table->triggers->create_trigger(thd, &stmt_query);
|
|
}
|
|
else
|
|
{
|
|
bool trigger_found;
|
|
|
|
result= table->triggers->drop_trigger(thd,
|
|
thd->lex->spname->m_name,
|
|
&trigger_found);
|
|
|
|
if (!result && trigger_found)
|
|
result= stmt_query.append(thd->query().str, thd->query().length);
|
|
}
|
|
|
|
if (result)
|
|
goto end;
|
|
|
|
close_all_tables_for_name(thd, table->s, false, NULL);
|
|
/*
|
|
Reopen the table if we were under LOCK TABLES.
|
|
Ignore the return value for now. It's better to
|
|
keep master/slave in consistent state.
|
|
*/
|
|
thd->locked_tables_list.reopen_tables(thd);
|
|
|
|
/*
|
|
Invalidate SP-cache. That's needed because triggers may change list of
|
|
pre-locking tables.
|
|
*/
|
|
sp_cache_invalidate();
|
|
|
|
end:
|
|
if (!result)
|
|
{
|
|
if (tables)
|
|
thd->add_to_binlog_accessed_dbs(tables->db);
|
|
result= write_bin_log(thd, TRUE, stmt_query.ptr(), stmt_query.length());
|
|
}
|
|
|
|
/*
|
|
If we are under LOCK TABLES we should restore original state of
|
|
meta-data locks. Otherwise all locks will be released along
|
|
with the implicit commit.
|
|
*/
|
|
if (thd->locked_tables_mode && tables && lock_upgrade_done)
|
|
mdl_ticket->downgrade_lock(MDL_SHARED_NO_READ_WRITE);
|
|
|
|
/* Restore the query table list. Used only for drop trigger. */
|
|
if (!create)
|
|
thd->lex->restore_backup_query_tables_list(&backup);
|
|
|
|
if (!result)
|
|
{
|
|
#ifdef HAVE_PSI_SP_INTERFACE
|
|
/* Drop statistics for this stored program from performance schema. */
|
|
MYSQL_DROP_SP(SP_TYPE_TRIGGER,
|
|
thd->lex->spname->m_db.str, thd->lex->spname->m_db.length,
|
|
thd->lex->spname->m_name.str, thd->lex->spname->m_name.length);
|
|
#endif
|
|
my_ok(thd);
|
|
}
|
|
|
|
DBUG_RETURN(result);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
Find trigger's table from trigger identifier and add it to
|
|
the statement table list.
|
|
|
|
@param[in] thd Thread context.
|
|
@param[in] trg_name Trigger name.
|
|
@param[in] if_exists TRUE if SQL statement contains "IF EXISTS" clause.
|
|
That means a warning instead of error should be
|
|
thrown if trigger with given name does not exist.
|
|
@param[out] table Pointer to TABLE_LIST object for the
|
|
table trigger.
|
|
|
|
@return Operation status
|
|
@retval FALSE On success.
|
|
@retval TRUE Otherwise.
|
|
*/
|
|
|
|
bool add_table_for_trigger(THD *thd,
|
|
const LEX_CSTRING &db_name,
|
|
const LEX_STRING &trigger_name,
|
|
bool continue_if_not_exist,
|
|
TABLE_LIST **table)
|
|
{
|
|
LEX *lex= thd->lex;
|
|
char trn_path_buff[FN_REFLEN];
|
|
LEX_STRING tbl_name= { NULL, 0 };
|
|
|
|
DBUG_ENTER("add_table_for_trigger");
|
|
|
|
LEX_STRING trn_path=
|
|
Trigger_loader::build_trn_path(trn_path_buff, FN_REFLEN,
|
|
db_name.str, trigger_name.str);
|
|
|
|
if (Trigger_loader::check_trn_exists(trn_path))
|
|
{
|
|
if (continue_if_not_exist)
|
|
{
|
|
push_warning(thd, Sql_condition::SL_NOTE,
|
|
ER_TRG_DOES_NOT_EXIST, ER(ER_TRG_DOES_NOT_EXIST));
|
|
|
|
*table= NULL;
|
|
|
|
DBUG_RETURN(FALSE);
|
|
}
|
|
|
|
my_error(ER_TRG_DOES_NOT_EXIST, MYF(0));
|
|
DBUG_RETURN(TRUE);
|
|
}
|
|
|
|
if (Trigger_loader::load_trn_file(thd, trigger_name, trn_path, &tbl_name))
|
|
DBUG_RETURN(TRUE);
|
|
|
|
*table= sp_add_to_query_tables(thd, lex, db_name.str, tbl_name.str);
|
|
|
|
DBUG_RETURN(*table ? FALSE : TRUE);
|
|
}
|
|
|
|
|
|
/**
|
|
Update .TRG and .TRN files after renaming triggers' subject table.
|
|
|
|
@param[i]] thd Thread context
|
|
@param[in] db_name Current database of subject table
|
|
@param[in] table_alias Current alias of subject table
|
|
@param[in] table_name Current name of subject table
|
|
@param[in] new_db_name New database for subject table
|
|
@param[in] new_table_name New name of subject table
|
|
|
|
@note
|
|
This method tries to leave trigger related files in consistent state, i.e.
|
|
it either will complete successfully, or will fail leaving files in their
|
|
initial state.
|
|
|
|
@note
|
|
This method assumes that subject table is not renamed to itself.
|
|
|
|
@note
|
|
This method needs to be called under an exclusive table metadata lock.
|
|
|
|
@return Operation status.
|
|
@retval false Success
|
|
@retval true Failure
|
|
*/
|
|
|
|
bool change_trigger_table_name(THD *thd,
|
|
const char *db_name,
|
|
const char *table_alias,
|
|
const char *table_name,
|
|
const char *new_db_name,
|
|
const char *new_table_name)
|
|
{
|
|
// Check if there is at least one trigger for the given table.
|
|
|
|
if (!Trigger_loader::trg_file_exists(db_name, table_name))
|
|
return false;
|
|
|
|
/*
|
|
Since triggers should be in the same schema as their subject tables
|
|
moving table with them between two schemas raises too many questions.
|
|
(E.g. what should happen if in new schema we already have trigger
|
|
with same name ?).
|
|
|
|
In case of "ALTER DATABASE `#mysql50#db1` UPGRADE DATA DIRECTORY NAME"
|
|
we will be given table name with "#mysql50#" prefix
|
|
To remove this prefix we use check_n_cut_mysql50_prefix().
|
|
*/
|
|
|
|
bool upgrading50to51= false;
|
|
|
|
if (my_strcasecmp(table_alias_charset, db_name, new_db_name))
|
|
{
|
|
char dbname[NAME_LEN + 1];
|
|
if (check_n_cut_mysql50_prefix(db_name, dbname, sizeof(dbname)) &&
|
|
!my_strcasecmp(table_alias_charset, dbname, new_db_name))
|
|
{
|
|
upgrading50to51= true;
|
|
}
|
|
else
|
|
{
|
|
my_error(ER_TRG_IN_WRONG_SCHEMA, MYF(0));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
This method interfaces the mysql server code protected by
|
|
an exclusive metadata lock.
|
|
*/
|
|
assert(thd->mdl_context.owns_equal_or_stronger_lock(MDL_key::TABLE,
|
|
db_name,
|
|
table_name,
|
|
MDL_EXCLUSIVE));
|
|
|
|
assert(my_strcasecmp(table_alias_charset, db_name, new_db_name) ||
|
|
my_strcasecmp(table_alias_charset, table_alias, new_table_name));
|
|
|
|
Table_trigger_dispatcher d(db_name, table_name);
|
|
|
|
return d.check_n_load(thd, true) ||
|
|
d.check_for_broken_triggers() ||
|
|
d.rename_subject_table(thd,
|
|
db_name, new_db_name,
|
|
table_alias,
|
|
new_table_name,
|
|
upgrading50to51);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
Drop all triggers for table.
|
|
|
|
@param thd current thread context
|
|
@param db_name name of the table schema
|
|
@param table_name table name
|
|
|
|
@return Operation status.
|
|
@retval false Success
|
|
@retval true Failure
|
|
*/
|
|
bool drop_all_triggers(THD *thd, const char *db_name, const char *table_name)
|
|
{
|
|
// Check if there is at least one trigger for the given table.
|
|
|
|
if (!Trigger_loader::trg_file_exists(db_name, table_name))
|
|
return false;
|
|
|
|
/*
|
|
Here we have to 1) load trigger definitions from TRG-files and 2) parse them
|
|
to find out trigger names. Since trigger names are not stored in the
|
|
TRG-file, it is impossible to avoid parsing just to delete triggers.
|
|
*/
|
|
|
|
Table_trigger_dispatcher d(db_name, table_name);
|
|
|
|
return
|
|
d.check_n_load(thd, true) ||
|
|
Trigger_loader::drop_all_triggers(db_name, table_name,
|
|
&d.get_trigger_list());
|
|
}
|