Commit 5ab4b6f1 authored by kostja@bodhi.(none)'s avatar kostja@bodhi.(none)

A fix and a test case for Bug#26141 mixing table types in trigger

causes full table lock on innodb table.
Also fixes Bug#28502 Triggers that update another innodb table 
will block on X lock unnecessarily (duplciate).
Code review fixes.

Both bugs' synopses are misleading: InnoDB table is
not X locked. The statements, however, cannot proceed concurrently, 
but this happens due to lock conflicts for tables used in triggers,
not for the InnoDB table. 

If a user had an InnoDB table, and two triggers, AFTER UPDATE and 
AFTER INSERT, competing for different resources (e.g. two distinct
MyISAM tables), then these two triggers would not be able to execute
concurrently. Moreover, INSERTS/UPDATES of the InnoDB table would
not be able to run concurrently. 
The problem had other side-effects (see respective bug reports).

This behavior was a consequence of a shortcoming of the pre-locking
algorithm, which would not distinguish between different DML operations
(e.g. INSERT and DELETE) and pre-lock all the tables
that are used by any trigger defined on the subject table.

The idea of the fix is to extend the pre-locking algorithm to keep track,
for each table, what DML operation it is used for and not
load triggers that are known to never be fired.
parent 392b283f
...@@ -82,3 +82,62 @@ ALICE 33 1 0 ...@@ -82,3 +82,62 @@ ALICE 33 1 0
THE CROWN 43 1 0 THE CROWN 43 1 0
THE PIE 53 1 1 THE PIE 53 1 1
drop table t1; drop table t1;
Bug#26141 mixing table types in trigger causes full
table lock on innodb table
Ensure we do not open and lock tables for the triggers we do not
fire.
drop table if exists t1, t2, t3;
drop trigger if exists trg_bug26141_au;
drop trigger if exists trg_bug26141_ai;
create table t1 (c int primary key) engine=innodb;
create table t2 (c int) engine=myisam;
create table t3 (c int) engine=myisam;
insert into t1 (c) values (1);
create trigger trg_bug26141_ai after insert on t1
for each row
begin
insert into t2 (c) values (1);
# We need the 'sync' lock to synchronously wait in connection 2 till
# the moment when the trigger acquired all the locks.
select release_lock("lock_bug26141_sync") into @a;
# 1000 is time in seconds of lock wait timeout -- this is a way
# to cause a manageable sleep up to 1000 seconds
select get_lock("lock_bug26141_wait", 1000) into @a;
end|
create trigger trg_bug26141_au after update on t1
for each row
begin
insert into t3 (c) values (1);
end|
select get_lock("lock_bug26141_wait", 0);
get_lock("lock_bug26141_wait", 0)
1
select get_lock("lock_bug26141_sync", /* must not be priorly locked */ 0);
get_lock("lock_bug26141_sync", /* must not be priorly locked */ 0)
1
insert into t1 (c) values (2);
select get_lock("lock_bug26141_sync", 1000);
get_lock("lock_bug26141_sync", 1000)
1
update t1 set c=3 where c=1;
select release_lock("lock_bug26141_sync");
release_lock("lock_bug26141_sync")
1
select release_lock("lock_bug26141_wait");
release_lock("lock_bug26141_wait")
1
select * from t1;
c
2
3
select * from t2;
c
1
select * from t3;
c
1
drop table t1, t2, t3;
End of 5.0 tests
This diff is collapsed.
...@@ -49,4 +49,84 @@ insert into t1 values ('The Pie', 50, 1, 1); ...@@ -49,4 +49,84 @@ insert into t1 values ('The Pie', 50, 1, 1);
select * from t1; select * from t1;
drop table t1; drop table t1;
# End of 5.0 tests --echo
--echo Bug#26141 mixing table types in trigger causes full
--echo table lock on innodb table
--echo
--echo Ensure we do not open and lock tables for the triggers we do not
--echo fire.
--echo
--disable_warnings
drop table if exists t1, t2, t3;
drop trigger if exists trg_bug26141_au;
drop trigger if exists trg_bug26141_ai;
--enable_warnings
# Note, for InnoDB to allow concurrent UPDATE and INSERT the
# table must have a unique key.
create table t1 (c int primary key) engine=innodb;
create table t2 (c int) engine=myisam;
create table t3 (c int) engine=myisam;
insert into t1 (c) values (1);
delimiter |;
create trigger trg_bug26141_ai after insert on t1
for each row
begin
insert into t2 (c) values (1);
# We need the 'sync' lock to synchronously wait in connection 2 till
# the moment when the trigger acquired all the locks.
select release_lock("lock_bug26141_sync") into @a;
# 1000 is time in seconds of lock wait timeout -- this is a way
# to cause a manageable sleep up to 1000 seconds
select get_lock("lock_bug26141_wait", 1000) into @a;
end|
create trigger trg_bug26141_au after update on t1
for each row
begin
insert into t3 (c) values (1);
end|
delimiter ;|
# Establish an alternative connection.
--connect (connection_aux,localhost,root,,test,,)
--connect (connection_update,localhost,root,,test,,)
connection connection_aux;
# Lock the wait lock, it must not be locked, so specify zero timeout.
select get_lock("lock_bug26141_wait", 0);
#
connection default;
#
# Run the trigger synchronously
#
select get_lock("lock_bug26141_sync", /* must not be priorly locked */ 0);
# Will acquire the table level locks, perform the insert into t2,
# release the sync lock and block on the wait lock.
send insert into t1 (c) values (2);
connection connection_update;
# Wait for the trigger to acquire its locks and unlock the sync lock.
select get_lock("lock_bug26141_sync", 1000);
#
# This must continue: after the fix for the bug, we do not
# open tables for t2, and with c=4 innobase allows the update
# to run concurrently with insert.
update t1 set c=3 where c=1;
select release_lock("lock_bug26141_sync");
connection connection_aux;
select release_lock("lock_bug26141_wait");
connection default;
reap;
select * from t1;
select * from t2;
select * from t3;
# Drops the trigger as well.
drop table t1, t2, t3;
disconnect connection_update;
disconnect connection_aux;
--echo End of 5.0 tests
...@@ -1828,5 +1828,370 @@ DROP TRIGGER t1_test; ...@@ -1828,5 +1828,370 @@ DROP TRIGGER t1_test;
DROP TABLE t1,t2; DROP TABLE t1,t2;
SET SESSION LOW_PRIORITY_UPDATES=DEFAULT; SET SESSION LOW_PRIORITY_UPDATES=DEFAULT;
SET GLOBAL LOW_PRIORITY_UPDATES=DEFAULT; SET GLOBAL LOW_PRIORITY_UPDATES=DEFAULT;
--echo
--echo Bug#28502 Triggers that update another innodb table will block
--echo on X lock unnecessarily
--echo
--echo Ensure we do not open and lock tables for triggers we do not fire.
--echo
--disable_warnings
drop table if exists t1, t2;
drop trigger if exists trg_bug28502_au;
--enable_warnings
create table t1 (id int, count int);
create table t2 (id int);
delimiter |;
create trigger trg_bug28502_au before update on t2
for each row
begin
if (new.id is not null) then
update t1 set count= count + 1 where id = old.id;
end if;
end|
delimiter ;|
insert into t1 (id, count) values (1, 0);
lock table t1 write;
--connect (connection_insert, localhost, root, , test, , )
connection connection_insert;
# Is expected to pass.
insert into t2 set id=1;
connection default;
unlock tables;
update t2 set id=1 where id=1;
select * from t1;
select * from t2;
# Will drop the trigger
drop table t1, t2;
disconnect connection_insert;
--echo
--echo Additionally, provide test coverage for triggers and
--echo all MySQL data changing commands.
--echo
--disable_warnings
drop table if exists t1, t2, t1_op_log;
drop view if exists v1;
drop trigger if exists trg_bug28502_bi;
drop trigger if exists trg_bug28502_ai;
drop trigger if exists trg_bug28502_bu;
drop trigger if exists trg_bug28502_au;
drop trigger if exists trg_bug28502_bd;
drop trigger if exists trg_bug28502_ad;
--enable_warnings
create table t1 (id int primary key auto_increment, operation varchar(255));
create table t2 (id int primary key);
create table t1_op_log(operation varchar(255));
create view v1 as select * from t1;
create trigger trg_bug28502_bi before insert on t1
for each row
insert into t1_op_log (operation)
values (concat("Before INSERT, new=", new.operation));
create trigger trg_bug28502_ai after insert on t1
for each row
insert into t1_op_log (operation)
values (concat("After INSERT, new=", new.operation));
create trigger trg_bug28502_bu before update on t1
for each row
insert into t1_op_log (operation)
values (concat("Before UPDATE, new=", new.operation,
", old=", old.operation));
create trigger trg_bug28502_au after update on t1
for each row
insert into t1_op_log (operation)
values (concat("After UPDATE, new=", new.operation,
", old=", old.operation));
create trigger trg_bug28502_bd before delete on t1
for each row
insert into t1_op_log (operation)
values (concat("Before DELETE, old=", old.operation));
create trigger trg_bug28502_ad after delete on t1
for each row
insert into t1_op_log (operation)
values (concat("After DELETE, old=", old.operation));
insert into t1 (operation) values ("INSERT");
set @id=last_insert_id();
select * from t1;
select * from t1_op_log;
truncate t1_op_log;
update t1 set operation="UPDATE" where id=@id;
select * from t1;
select * from t1_op_log;
truncate t1_op_log;
delete from t1 where id=@id;
select * from t1;
select * from t1_op_log;
truncate t1;
truncate t1_op_log;
insert into t1 (id, operation) values
(NULL, "INSERT ON DUPLICATE KEY UPDATE, inserting a new key")
on duplicate key update id=NULL, operation="Should never happen";
set @id=last_insert_id();
select * from t1;
select * from t1_op_log;
truncate t1_op_log;
insert into t1 (id, operation) values
(@id, "INSERT ON DUPLICATE KEY UPDATE, the key value is the same")
on duplicate key update id=NULL,
operation="INSERT ON DUPLICATE KEY UPDATE, updating the duplicate";
select * from t1;
select * from t1_op_log;
truncate t1;
truncate t1_op_log;
replace into t1 values (NULL, "REPLACE, inserting a new key");
set @id=last_insert_id();
select * from t1;
select * from t1_op_log;
truncate t1_op_log;
replace into t1 values (@id, "REPLACE, deleting the duplicate");
select * from t1;
select * from t1_op_log;
truncate t1;
truncate t1_op_log;
create table if not exists t1
select NULL, "CREATE TABLE ... SELECT, inserting a new key";
set @id=last_insert_id();
select * from t1;
select * from t1_op_log;
truncate t1_op_log;
create table if not exists t1 replace
select @id, "CREATE TABLE ... REPLACE SELECT, deleting a duplicate key";
select * from t1;
select * from t1_op_log;
truncate t1;
truncate t1_op_log;
insert into t1 (id, operation)
select NULL, "INSERT ... SELECT, inserting a new key";
set @id=last_insert_id();
select * from t1;
select * from t1_op_log;
truncate t1_op_log;
insert into t1 (id, operation)
select @id,
"INSERT ... SELECT ... ON DUPLICATE KEY UPDATE, updating a duplicate"
on duplicate key update id=NULL,
operation="INSERT ... SELECT ... ON DUPLICATE KEY UPDATE, updating a duplicate";
select * from t1;
select * from t1_op_log;
truncate t1;
truncate t1_op_log;
replace into t1 (id, operation)
select NULL, "REPLACE ... SELECT, inserting a new key";
set @id=last_insert_id();
select * from t1;
select * from t1_op_log;
truncate t1_op_log;
replace into t1 (id, operation)
select @id, "REPLACE ... SELECT, deleting a duplicate";
select * from t1;
select * from t1_op_log;
truncate t1;
truncate t1_op_log;
insert into t1 (id, operation) values (1, "INSERT for multi-DELETE");
insert into t2 (id) values (1);
delete t1.*, t2.* from t1, t2 where t1.id=1;
select * from t1;
select * from t2;
select * from t1_op_log;
truncate t1;
truncate t2;
truncate t1_op_log;
insert into t1 (id, operation) values (1, "INSERT for multi-UPDATE");
insert into t2 (id) values (1);
update t1, t2 set t1.id=2, operation="multi-UPDATE" where t1.id=1;
update t1, t2
set t2.id=3, operation="multi-UPDATE, SET for t2, but the trigger is fired" where t1.id=2;
select * from t1;
select * from t2;
select * from t1_op_log;
truncate table t1;
truncate table t2;
truncate table t1_op_log;
--echo
--echo Now do the same but use a view instead of the base table.
--echo
insert into v1 (operation) values ("INSERT");
set @id=last_insert_id();
select * from t1;
select * from t1_op_log;
truncate t1_op_log;
update v1 set operation="UPDATE" where id=@id;
select * from t1;
select * from t1_op_log;
truncate t1_op_log;
delete from v1 where id=@id;
select * from t1;
select * from t1_op_log;
truncate t1;
truncate t1_op_log;
insert into v1 (id, operation) values
(NULL, "INSERT ON DUPLICATE KEY UPDATE, inserting a new key")
on duplicate key update id=NULL, operation="Should never happen";
set @id=last_insert_id();
select * from t1;
select * from t1_op_log;
truncate t1_op_log;
insert into v1 (id, operation) values
(@id, "INSERT ON DUPLICATE KEY UPDATE, the key value is the same")
on duplicate key update id=NULL,
operation="INSERT ON DUPLICATE KEY UPDATE, updating the duplicate";
select * from t1;
select * from t1_op_log;
truncate t1;
truncate t1_op_log;
replace into v1 values (NULL, "REPLACE, inserting a new key");
set @id=last_insert_id();
select * from t1;
select * from t1_op_log;
truncate t1_op_log;
replace into v1 values (@id, "REPLACE, deleting the duplicate");
select * from t1;
select * from t1_op_log;
truncate t1;
truncate t1_op_log;
create table if not exists v1
select NULL, "CREATE TABLE ... SELECT, inserting a new key";
set @id=last_insert_id();
select * from t1;
select * from t1_op_log;
truncate t1_op_log;
create table if not exists v1 replace
select @id, "CREATE TABLE ... REPLACE SELECT, deleting a duplicate key";
select * from t1;
select * from t1_op_log;
truncate t1;
truncate t1_op_log;
insert into v1 (id, operation)
select NULL, "INSERT ... SELECT, inserting a new key";
set @id=last_insert_id();
select * from t1;
select * from t1_op_log;
truncate t1_op_log;
insert into v1 (id, operation)
select @id,
"INSERT ... SELECT ... ON DUPLICATE KEY UPDATE, updating a duplicate"
on duplicate key update id=NULL,
operation="INSERT ... SELECT ... ON DUPLICATE KEY UPDATE, updating a duplicate";
select * from t1;
select * from t1_op_log;
truncate t1;
truncate t1_op_log;
replace into v1 (id, operation)
select NULL, "REPLACE ... SELECT, inserting a new key";
set @id=last_insert_id();
select * from t1;
select * from t1_op_log;
truncate t1_op_log;
replace into v1 (id, operation)
select @id, "REPLACE ... SELECT, deleting a duplicate";
select * from t1;
select * from t1_op_log;
truncate t1;
truncate t1_op_log;
insert into v1 (id, operation) values (1, "INSERT for multi-DELETE");
insert into t2 (id) values (1);
delete v1.*, t2.* from v1, t2 where v1.id=1;
select * from t1;
select * from t2;
select * from t1_op_log;
truncate t1;
truncate t2;
truncate t1_op_log;
insert into v1 (id, operation) values (1, "INSERT for multi-UPDATE");
insert into t2 (id) values (1);
update v1, t2 set v1.id=2, operation="multi-UPDATE" where v1.id=1;
update v1, t2
set t2.id=3, operation="multi-UPDATE, SET for t2, but the trigger is fired" where v1.id=2;
select * from t1;
select * from t2;
select * from t1_op_log;
drop view v1;
drop table t1, t2, t1_op_log;
#
# TODO: test LOAD DATA INFILE
--echo End of 5.0 tests --echo End of 5.0 tests
...@@ -2309,14 +2309,6 @@ enum trg_action_time_type ...@@ -2309,14 +2309,6 @@ enum trg_action_time_type
TRG_ACTION_BEFORE= 0, TRG_ACTION_AFTER= 1, TRG_ACTION_MAX TRG_ACTION_BEFORE= 0, TRG_ACTION_AFTER= 1, TRG_ACTION_MAX
}; };
/*
Event on which trigger is invoked.
*/
enum trg_event_type
{
TRG_EVENT_INSERT= 0 , TRG_EVENT_UPDATE= 1, TRG_EVENT_DELETE= 2, TRG_EVENT_MAX
};
class Table_triggers_list; class Table_triggers_list;
/* /*
......
...@@ -440,6 +440,19 @@ db_load_routine(THD *thd, int type, sp_name *name, sp_head **sphp, ...@@ -440,6 +440,19 @@ db_load_routine(THD *thd, int type, sp_name *name, sp_head **sphp,
lex_start(thd); lex_start(thd);
thd->spcont= NULL; thd->spcont= NULL;
ret= MYSQLparse(thd); ret= MYSQLparse(thd);
if (ret == 0)
{
/*
Not strictly necessary to invoke this method here, since we know
that we've parsed CREATE PROCEDURE/FUNCTION and not an
UPDATE/DELETE/INSERT/REPLACE/LOAD/CREATE TABLE, but we try to
maintain the invariant that this method is called for each
distinct statement, in case its logic is extended with other
types of analyses in future.
*/
newlex.set_trg_event_type_for_tables();
}
} }
if (ret || thd->is_fatal_error || newlex.sphead == NULL) if (ret || thd->is_fatal_error || newlex.sphead == NULL)
...@@ -1742,31 +1755,39 @@ sp_cache_routines_and_add_tables_for_triggers(THD *thd, LEX *lex, ...@@ -1742,31 +1755,39 @@ sp_cache_routines_and_add_tables_for_triggers(THD *thd, LEX *lex,
TABLE_LIST *table) TABLE_LIST *table)
{ {
int ret= 0; int ret= 0;
Table_triggers_list *triggers= table->table->triggers;
if (add_used_routine(lex, thd->stmt_arena, &triggers->sroutines_key, Sroutine_hash_entry **last_cached_routine_ptr=
table->belong_to_view)) (Sroutine_hash_entry **)lex->sroutines_list.next;
if (static_cast<int>(table->lock_type) >=
static_cast<int>(TL_WRITE_ALLOW_WRITE))
{ {
Sroutine_hash_entry **last_cached_routine_ptr=
(Sroutine_hash_entry **)lex->sroutines_list.next;
for (int i= 0; i < (int)TRG_EVENT_MAX; i++) for (int i= 0; i < (int)TRG_EVENT_MAX; i++)
{ {
for (int j= 0; j < (int)TRG_ACTION_MAX; j++) if (table->trg_event_map &
static_cast<uint8>(1 << static_cast<int>(i)))
{ {
if (triggers->bodies[i][j]) for (int j= 0; j < (int)TRG_ACTION_MAX; j++)
{ {
(void)triggers->bodies[i][j]-> /* We can have only one trigger per action type currently */
add_used_tables_to_table_list(thd, &lex->query_tables_last, sp_head *trigger= table->table->triggers->bodies[i][j];
table->belong_to_view); if (trigger &&
sp_update_stmt_used_routines(thd, lex, add_used_routine(lex, thd->stmt_arena, &trigger->m_sroutines_key,
&triggers->bodies[i][j]->m_sroutines, table->belong_to_view))
table->belong_to_view); {
trigger->add_used_tables_to_table_list(thd, &lex->query_tables_last,
table->belong_to_view);
sp_update_stmt_used_routines(thd, lex,
&trigger->m_sroutines,
table->belong_to_view);
}
} }
} }
} }
ret= sp_cache_routines_and_add_tables_aux(thd, lex,
*last_cached_routine_ptr,
FALSE, NULL);
} }
ret= sp_cache_routines_and_add_tables_aux(thd, lex,
*last_cached_routine_ptr,
FALSE, NULL);
return ret; return ret;
} }
......
...@@ -478,12 +478,35 @@ sp_head::init(LEX *lex) ...@@ -478,12 +478,35 @@ sp_head::init(LEX *lex)
*/ */
lex->trg_table_fields.empty(); lex->trg_table_fields.empty();
my_init_dynamic_array(&m_instr, sizeof(sp_instr *), 16, 8); my_init_dynamic_array(&m_instr, sizeof(sp_instr *), 16, 8);
m_param_begin= m_param_end= m_body_begin= 0;
m_qname.str= m_db.str= m_name.str= m_params.str= m_param_begin= NULL;
m_body.str= m_defstr.str= 0; m_param_end= NULL;
m_qname.length= m_db.length= m_name.length= m_params.length=
m_body.length= m_defstr.length= 0; m_body_begin= NULL ;
m_qname.str= NULL;
m_qname.length= 0;
m_db.str= NULL;
m_db.length= 0;
m_name.str= NULL;
m_name.length= 0;
m_params.str= NULL;
m_params.length= 0;
m_body.str= NULL;
m_body.length= 0;
m_defstr.str= NULL;
m_defstr.length= 0;
m_sroutines_key.str= NULL;
m_sroutines_key.length= 0;
m_return_field_def.charset= NULL; m_return_field_def.charset= NULL;
DBUG_VOID_RETURN; DBUG_VOID_RETURN;
} }
...@@ -509,9 +532,14 @@ sp_head::init_sp_name(THD *thd, sp_name *spname) ...@@ -509,9 +532,14 @@ sp_head::init_sp_name(THD *thd, sp_name *spname)
if (spname->m_qname.length == 0) if (spname->m_qname.length == 0)
spname->init_qname(thd); spname->init_qname(thd);
m_qname.length= spname->m_qname.length; m_sroutines_key.length= spname->m_sroutines_key.length;
m_qname.str= strmake_root(thd->mem_root, spname->m_qname.str, m_sroutines_key.str= memdup_root(thd->mem_root,
m_qname.length); spname->m_sroutines_key.str,
spname->m_sroutines_key.length + 1);
m_sroutines_key.str[0]= static_cast<char>(m_type);
m_qname.length= m_sroutines_key.length - 1;
m_qname.str= m_sroutines_key.str + 1;
DBUG_VOID_RETURN; DBUG_VOID_RETURN;
} }
...@@ -1796,8 +1824,11 @@ sp_head::restore_lex(THD *thd) ...@@ -1796,8 +1824,11 @@ sp_head::restore_lex(THD *thd)
{ {
DBUG_ENTER("sp_head::restore_lex"); DBUG_ENTER("sp_head::restore_lex");
LEX *sublex= thd->lex; LEX *sublex= thd->lex;
LEX *oldlex= (LEX *)m_lex.pop(); LEX *oldlex;
sublex->set_trg_event_type_for_tables();
oldlex= (LEX *)m_lex.pop();
if (! oldlex) if (! oldlex)
return; // Nothing to restore return; // Nothing to restore
...@@ -3429,6 +3460,7 @@ typedef struct st_sp_table ...@@ -3429,6 +3460,7 @@ typedef struct st_sp_table
thr_lock_type lock_type; /* lock type used for prelocking */ thr_lock_type lock_type; /* lock type used for prelocking */
uint lock_count; uint lock_count;
uint query_lock_count; uint query_lock_count;
uint8 trg_event_map;
} SP_TABLE; } SP_TABLE;
byte * byte *
...@@ -3515,6 +3547,7 @@ sp_head::merge_table_list(THD *thd, TABLE_LIST *table, LEX *lex_for_tmp_check) ...@@ -3515,6 +3547,7 @@ sp_head::merge_table_list(THD *thd, TABLE_LIST *table, LEX *lex_for_tmp_check)
tab->query_lock_count++; tab->query_lock_count++;
if (tab->query_lock_count > tab->lock_count) if (tab->query_lock_count > tab->lock_count)
tab->lock_count++; tab->lock_count++;
tab->trg_event_map|= table->trg_event_map;
} }
else else
{ {
...@@ -3536,6 +3569,7 @@ sp_head::merge_table_list(THD *thd, TABLE_LIST *table, LEX *lex_for_tmp_check) ...@@ -3536,6 +3569,7 @@ sp_head::merge_table_list(THD *thd, TABLE_LIST *table, LEX *lex_for_tmp_check)
tab->db_length= table->db_length; tab->db_length= table->db_length;
tab->lock_type= table->lock_type; tab->lock_type= table->lock_type;
tab->lock_count= tab->query_lock_count= 1; tab->lock_count= tab->query_lock_count= 1;
tab->trg_event_map= table->trg_event_map;
my_hash_insert(&m_sptabs, (byte *)tab); my_hash_insert(&m_sptabs, (byte *)tab);
} }
} }
...@@ -3613,6 +3647,7 @@ sp_head::add_used_tables_to_table_list(THD *thd, ...@@ -3613,6 +3647,7 @@ sp_head::add_used_tables_to_table_list(THD *thd,
table->cacheable_table= 1; table->cacheable_table= 1;
table->prelocking_placeholder= 1; table->prelocking_placeholder= 1;
table->belong_to_view= belong_to_view; table->belong_to_view= belong_to_view;
table->trg_event_map= stab->trg_event_map;
/* Everyting else should be zeroed */ /* Everyting else should be zeroed */
......
...@@ -130,6 +130,12 @@ public: ...@@ -130,6 +130,12 @@ public:
st_sp_chistics *m_chistics; st_sp_chistics *m_chistics;
ulong m_sql_mode; // For SHOW CREATE and execution ulong m_sql_mode; // For SHOW CREATE and execution
LEX_STRING m_qname; // db.name LEX_STRING m_qname; // db.name
/**
Key representing routine in the set of stored routines used by statement.
[routine_type]db.name\0
@sa sp_name::m_sroutines_key
*/
LEX_STRING m_sroutines_key;
LEX_STRING m_db; LEX_STRING m_db;
LEX_STRING m_name; LEX_STRING m_name;
LEX_STRING m_params; LEX_STRING m_params;
......
...@@ -2034,6 +2034,27 @@ void st_select_lex_unit::set_limit(SELECT_LEX *sl) ...@@ -2034,6 +2034,27 @@ void st_select_lex_unit::set_limit(SELECT_LEX *sl)
} }
/**
Update the parsed tree with information about triggers that
may be fired when executing this statement.
*/
void st_lex::set_trg_event_type_for_tables()
{
/*
Do not iterate over sub-selects, only the tables in the outermost
SELECT_LEX can be modified, if any.
*/
TABLE_LIST *tables= select_lex.get_table_list();
while (tables)
{
tables->set_trg_event_type(this);
tables= tables->next_local;
}
}
/* /*
Unlink the first table from the global table list and the first table from Unlink the first table from the global table list and the first table from
outer select (lex->select_lex) local list outer select (lex->select_lex) local list
......
...@@ -1188,6 +1188,8 @@ typedef struct st_lex : public Query_tables_list ...@@ -1188,6 +1188,8 @@ typedef struct st_lex : public Query_tables_list
un->uncacheable|= cause; un->uncacheable|= cause;
} }
} }
void set_trg_event_type_for_tables();
TABLE_LIST *unlink_first_table(bool *link_to_local); TABLE_LIST *unlink_first_table(bool *link_to_local);
void link_first_table_back(TABLE_LIST *first, bool link_to_local); void link_first_table_back(TABLE_LIST *first, bool link_to_local);
void first_lists_tables_same(); void first_lists_tables_same();
......
...@@ -6080,8 +6080,9 @@ void mysql_parse(THD *thd, const char *inBuf, uint length, ...@@ -6080,8 +6080,9 @@ void mysql_parse(THD *thd, const char *inBuf, uint length,
(thd->query_length= (ulong)(lip.found_semicolon - thd->query))) (thd->query_length= (ulong)(lip.found_semicolon - thd->query)))
thd->query_length--; thd->query_length--;
/* Actually execute the query */ /* Actually execute the query */
mysql_execute_command(thd); lex->set_trg_event_type_for_tables();
query_cache_end_of_result(thd); mysql_execute_command(thd);
query_cache_end_of_result(thd);
} }
} }
} }
......
...@@ -2826,6 +2826,7 @@ bool Prepared_statement::prepare(const char *packet, uint packet_len) ...@@ -2826,6 +2826,7 @@ bool Prepared_statement::prepare(const char *packet, uint packet_len)
lex_start(thd); lex_start(thd);
lex->safe_to_cache_query= FALSE; lex->safe_to_cache_query= FALSE;
int err= MYSQLparse((void *)thd); int err= MYSQLparse((void *)thd);
lex->set_trg_event_type_for_tables();
error= err || thd->is_fatal_error || error= err || thd->is_fatal_error ||
thd->net.report_error || init_param_array(this); thd->net.report_error || init_param_array(this);
......
...@@ -942,17 +942,6 @@ bool Table_triggers_list::check_n_load(THD *thd, const char *db, ...@@ -942,17 +942,6 @@ bool Table_triggers_list::check_n_load(THD *thd, const char *db,
table->triggers= triggers; table->triggers= triggers;
/*
Construct key that will represent triggers for this table in the set
of routines used by statement.
*/
triggers->sroutines_key.length= 1+strlen(db)+1+strlen(table_name)+1;
if (!(triggers->sroutines_key.str=
alloc_root(&table->mem_root, triggers->sroutines_key.length)))
DBUG_RETURN(1);
triggers->sroutines_key.str[0]= TYPE_ENUM_TRIGGER;
strxmov(triggers->sroutines_key.str+1, db, ".", table_name, NullS);
/* /*
TODO: This could be avoided if there is no triggers TODO: This could be avoided if there is no triggers
for UPDATE and DELETE. for UPDATE and DELETE.
...@@ -991,6 +980,15 @@ bool Table_triggers_list::check_n_load(THD *thd, const char *db, ...@@ -991,6 +980,15 @@ bool Table_triggers_list::check_n_load(THD *thd, const char *db,
DBUG_ASSERT(lex.sphead == 0); DBUG_ASSERT(lex.sphead == 0);
goto err_with_lex_cleanup; goto err_with_lex_cleanup;
} }
/*
Not strictly necessary to invoke this method here, since we know
that we've parsed CREATE TRIGGER and not an
UPDATE/DELETE/INSERT/REPLACE/LOAD/CREATE TABLE, but we try to
maintain the invariant that this method is called for each
distinct statement, in case its logic is extended with other
types of analyses in future.
*/
lex.set_trg_event_type_for_tables();
lex.sphead->set_info(0, 0, &lex.sp_chistics, (ulong) *trg_sql_mode); lex.sphead->set_info(0, 0, &lex.sp_chistics, (ulong) *trg_sql_mode);
...@@ -1550,6 +1548,12 @@ bool Table_triggers_list::process_triggers(THD *thd, trg_event_type event, ...@@ -1550,6 +1548,12 @@ bool Table_triggers_list::process_triggers(THD *thd, trg_event_type event,
new_field= record1_field; new_field= record1_field;
old_field= trigger_table->field; old_field= trigger_table->field;
} }
/*
This trigger must have been processed by the pre-locking
algorithm.
*/
DBUG_ASSERT(trigger_table->pos_in_table_list->trg_event_map &
static_cast<uint>(1 << static_cast<int>(event)));
thd->reset_sub_statement_state(&statement_state, SUB_STMT_TRIGGER); thd->reset_sub_statement_state(&statement_state, SUB_STMT_TRIGGER);
err_status= sp_trigger->execute_trigger err_status= sp_trigger->execute_trigger
...@@ -1568,7 +1572,7 @@ bool Table_triggers_list::process_triggers(THD *thd, trg_event_type event, ...@@ -1568,7 +1572,7 @@ bool Table_triggers_list::process_triggers(THD *thd, trg_event_type event,
SYNOPSIS SYNOPSIS
mark_fields_used() mark_fields_used()
thd Current thread context thd Current thread context
event Type of event triggers for which we are going to inspect event Type of event triggers for which we are going to ins
DESCRIPTION DESCRIPTION
This method marks fields of subject table which are read/set in its This method marks fields of subject table which are read/set in its
......
...@@ -56,14 +56,6 @@ class Table_triggers_list: public Sql_alloc ...@@ -56,14 +56,6 @@ class Table_triggers_list: public Sql_alloc
updating trigger definitions during RENAME TABLE. updating trigger definitions during RENAME TABLE.
*/ */
List<LEX_STRING> on_table_names_list; List<LEX_STRING> on_table_names_list;
/*
Key representing triggers for this table in set of all stored
routines used by statement.
TODO: We won't need this member once triggers namespace will be
database-wide instead of table-wide because then we will be able
to use key based on sp_name as for other stored routines.
*/
LEX_STRING sroutines_key;
/* /*
Grant information for each trigger (pair: subject table, trigger definer). Grant information for each trigger (pair: subject table, trigger definer).
......
...@@ -1153,7 +1153,20 @@ bool mysql_make_view(THD *thd, File_parser *parser, TABLE_LIST *table, ...@@ -1153,7 +1153,20 @@ bool mysql_make_view(THD *thd, File_parser *parser, TABLE_LIST *table,
*/ */
for (tbl= view_main_select_tables; tbl; tbl= tbl->next_local) for (tbl= view_main_select_tables; tbl; tbl= tbl->next_local)
tbl->lock_type= table->lock_type; tbl->lock_type= table->lock_type;
/*
If the view is mergeable, we might want to
INSERT/UPDATE/DELETE into tables of this view. Preserve the
original sql command and 'duplicates' of the outer lex.
This is used later in set_trg_event_type_for_command.
*/
lex->sql_command= old_lex->sql_command;
lex->duplicates= old_lex->duplicates;
} }
/*
This method has a dependency on the proper lock type being set,
so in case of views should be called here.
*/
lex->set_trg_event_type_for_tables();
/* /*
If we are opening this view as part of implicit LOCK TABLES, then If we are opening this view as part of implicit LOCK TABLES, then
......
...@@ -9644,13 +9644,13 @@ trigger_tail: ...@@ -9644,13 +9644,13 @@ trigger_tail:
MYSQL_YYABORT; MYSQL_YYABORT;
sp->reset_thd_mem_root(thd); sp->reset_thd_mem_root(thd);
sp->init(lex); sp->init(lex);
sp->m_type= TYPE_ENUM_TRIGGER;
sp->init_sp_name(thd, $3); sp->init_sp_name(thd, $3);
lex->stmt_definition_begin= $2; lex->stmt_definition_begin= $2;
lex->ident.str= $7; lex->ident.str= $7;
lex->ident.length= $10 - $7; lex->ident.length= $10 - $7;
sp->m_type= TYPE_ENUM_TRIGGER;
lex->sphead= sp; lex->sphead= sp;
lex->spname= $3; lex->spname= $3;
/* /*
...@@ -9728,9 +9728,9 @@ sp_tail: ...@@ -9728,9 +9728,9 @@ sp_tail:
sp= new sp_head(); sp= new sp_head();
sp->reset_thd_mem_root(YYTHD); sp->reset_thd_mem_root(YYTHD);
sp->init(lex); sp->init(lex);
sp->m_type= TYPE_ENUM_PROCEDURE;
sp->init_sp_name(YYTHD, $3); sp->init_sp_name(YYTHD, $3);
sp->m_type= TYPE_ENUM_PROCEDURE;
lex->sphead= sp; lex->sphead= sp;
/* /*
* We have to turn of CLIENT_MULTI_QUERIES while parsing a * We have to turn of CLIENT_MULTI_QUERIES while parsing a
......
...@@ -1776,6 +1776,135 @@ void st_table::reset_item_list(List<Item> *item_list) const ...@@ -1776,6 +1776,135 @@ void st_table::reset_item_list(List<Item> *item_list) const
} }
} }
/**
Set the initial purpose of this TABLE_LIST object in the list of
used tables. We need to track this information on table-by-
table basis, since when this table becomes an element of the
pre-locked list, it's impossible to identify which SQL
sub-statement it has been originally used in.
E.g.:
User request: SELECT * FROM t1 WHERE f1();
FUNCTION f1(): DELETE FROM t2; RETURN 1;
BEFORE DELETE trigger on t2: INSERT INTO t3 VALUES (old.a);
For this user request, the pre-locked list will contain t1, t2, t3
table elements, each needed for different DML.
This method is called immediately after parsing for tables
of the table list of the top-level select lex.
The trigger event map is updated to reflect INSERT, UPDATE, DELETE,
REPLACE, LOAD DATA, CREATE TABLE .. SELECT, CREATE TABLE ..
REPLACE SELECT statements, and additionally ON DUPLICATE KEY UPDATE
clause.
*/
void
TABLE_LIST::set_trg_event_type(const st_lex *lex)
{
enum trg_event_type trg_event;
/*
Some auxiliary operations
(e.g. GRANT processing) create TABLE_LIST instances outside
the parser. Additionally, some commands (e.g. OPTIMIZE) change
the lock type for a table only after parsing is done. Luckily,
these do not fire triggers and do not need to pre-load them.
For these TABLE_LISTs set_trg_event_type is never called, and
trg_event_map is always empty. That means that the pre-locking
algorithm will ignore triggers defined on these tables, if
any, and the execution will either fail with an assert in
sql_trigger.cc or with an error that a used table was not
pre-locked, in case of a production build.
TODO: this usage pattern creates unnecessary module dependencies
and should be rewritten to go through the parser.
Table list instances created outside the parser in most cases
refer to mysql.* system tables. It is not allowed to have
a trigger on a system table, but keeping track of
initialization provides extra safety in case this limitation
is circumvented.
*/
/*
This is a fast check to filter out statements that do
not change data, or tables on the right side, in case of
INSERT .. SELECT, CREATE TABLE .. SELECT and so on.
Here we also filter out OPTIMIZE statement and non-updateable
views, for which lock_type is TL_UNLOCK or TL_READ after
parsing.
*/
if (static_cast<int>(lock_type) < static_cast<int>(TL_WRITE_ALLOW_WRITE))
return;
switch (lex->sql_command) {
/*
Basic INSERT. If there is an additional ON DUPLIATE KEY UPDATE
clause, it will be handled later in this method.
*/
case SQLCOM_INSERT: /* fall through */
case SQLCOM_INSERT_SELECT:
/*
LOAD DATA ... INFILE is expected to fire BEFORE/AFTER INSERT
triggers.
If the statement also has REPLACE clause, it will be
handled later in this method.
*/
case SQLCOM_LOAD: /* fall through */
/*
REPLACE is semantically equivalent to INSERT. In case
of a primary or unique key conflict, it deletes the old
record and inserts a new one. So we also may need to
fire ON DELETE triggers. This functionality is handled
later in this method.
*/
case SQLCOM_REPLACE: /* fall through */
case SQLCOM_REPLACE_SELECT:
/*
CREATE TABLE ... SELECT defaults to INSERT if the table or
view already exists. REPLACE option of CREATE TABLE ...
REPLACE SELECT is handled later in this method.
*/
case SQLCOM_CREATE_TABLE:
trg_event= TRG_EVENT_INSERT;
break;
/* Basic update and multi-update */
case SQLCOM_UPDATE: /* fall through */
case SQLCOM_UPDATE_MULTI:
trg_event= TRG_EVENT_UPDATE;
break;
/* Basic delete and multi-delete */
case SQLCOM_DELETE: /* fall through */
case SQLCOM_DELETE_MULTI:
trg_event= TRG_EVENT_DELETE;
break;
default:
/*
OK to return, since value of 'duplicates' is irrelevant
for non-updating commands.
*/
return;
}
trg_event_map|= static_cast<uint8>(1 << static_cast<int>(trg_event));
switch (lex->duplicates) {
case DUP_UPDATE:
trg_event= TRG_EVENT_UPDATE;
break;
case DUP_REPLACE:
trg_event= TRG_EVENT_DELETE;
break;
case DUP_ERROR:
default:
return;
}
trg_event_map|= static_cast<uint8>(1 << static_cast<int>(trg_event));
}
/* /*
calculate md5 of query calculate md5 of query
......
...@@ -59,6 +59,17 @@ enum tmp_table_type {NO_TMP_TABLE=0, ...@@ -59,6 +59,17 @@ enum tmp_table_type {NO_TMP_TABLE=0,
NON_TRANSACTIONAL_TMP_TABLE=1, TRANSACTIONAL_TMP_TABLE=2, NON_TRANSACTIONAL_TMP_TABLE=1, TRANSACTIONAL_TMP_TABLE=2,
SYSTEM_TMP_TABLE=3}; SYSTEM_TMP_TABLE=3};
/** Event on which trigger is invoked. */
enum trg_event_type
{
TRG_EVENT_INSERT= 0,
TRG_EVENT_UPDATE= 1,
TRG_EVENT_DELETE= 2,
TRG_EVENT_MAX
};
enum frm_type_enum enum frm_type_enum
{ {
FRMTYPE_ERROR= 0, FRMTYPE_ERROR= 0,
...@@ -702,6 +713,13 @@ struct TABLE_LIST ...@@ -702,6 +713,13 @@ struct TABLE_LIST
*/ */
bool create; bool create;
/**
Indicates what triggers we need to pre-load for this TABLE_LIST
when opening an associated TABLE. This is filled after
the parsed tree is created.
*/
uint8 trg_event_map;
enum enum_schema_table_state schema_table_state; enum enum_schema_table_state schema_table_state;
void calc_md5(char *buffer); void calc_md5(char *buffer);
void set_underlying_merge(); void set_underlying_merge();
...@@ -752,6 +770,7 @@ struct TABLE_LIST ...@@ -752,6 +770,7 @@ struct TABLE_LIST
void reinit_before_use(THD *thd); void reinit_before_use(THD *thd);
Item_subselect *containing_subselect(); Item_subselect *containing_subselect();
void set_trg_event_type(const st_lex *lex);
private: private:
bool prep_check_option(THD *thd, uint8 check_opt_type); bool prep_check_option(THD *thd, uint8 check_opt_type);
bool prep_where(THD *thd, Item **conds, bool no_where_clause); bool prep_where(THD *thd, Item **conds, bool no_where_clause);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment