StoneDB/sql/sql_trigger.cc

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());
}