A fix and a test case for Bug#12713 "Error in a stored function called from

a SELECT doesn't cause ROLLBACK of statem".

The idea of the fix is to ensure that we always commit the current
statement at the end of dispatch_command(). In order to not issue
redundant disc syncs, an optimization of the two-phase commit
protocol is implemented to bypass the two phase commit if
the transaction is read-only.
This commit is contained in:
kostja@dipika.(none) 2008-02-19 14:43:01 +03:00
parent 48d326612a
commit acf9b1f346
27 changed files with 2514 additions and 247 deletions

View File

@ -0,0 +1,742 @@
## Bug#12713 (Error in a stored function called from a SELECT doesn't cause
## ROLLBACK of statem)
##
## Pre-Requisites :
## - $engine_type should be set
##
set sql_mode=no_engine_substitution;
eval set storage_engine = $engine_type;
set autocommit=1;
--disable_warnings
drop table if exists t1;
drop table if exists t2;
drop table if exists t3;
drop function if exists f2;
drop procedure if exists bug12713_call;
drop procedure if exists bug12713_dump_spvars;
drop procedure if exists dummy;
--enable_warnings
create table t1 (a int);
create table t2 (a int unique);
create table t3 (a int);
# a workaround for Bug#32633: Can not create any routine if
# SQL_MODE=no_engine_substitution
set sql_mode=default;
insert into t1 (a) values (1), (2);
insert into t3 (a) values (1), (2);
delimiter |;
## Cause a failure every time
create function f2(x int) returns int
begin
insert into t2 (a) values (x);
insert into t2 (a) values (x);
return x;
end|
delimiter ;|
set autocommit=0;
flush status;
##============================================================================
## Design notes
##
## In each case, statement rollback is expected.
## for transactional engines, the rollback should be properly executed
## for non transactional engines, the rollback may cause warnings.
##
## The test pattern is as follows
## - insert 1000+N
## - statement with a side effect, that fails to insert N twice
## - a statement rollback is expected (expecting 1 row 1000+N only) in t2
## - a rollback is performed
## - expecting a clean table t2.
##============================================================================
insert into t2 (a) values (1001);
--error ER_DUP_ENTRY
insert into t1 (a) values (f2(1));
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1002);
--error ER_DUP_ENTRY
insert into t3 (a) select f2(2) from t1;
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1003);
--error ER_DUP_ENTRY
update t1 set a= a + f2(3);
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1004);
--error ER_DUP_ENTRY
update t1, t3 set t1.a = 0, t3.a = 0 where (f2(4) = 4) and (t1.a = t3.a);
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1005);
--error ER_DUP_ENTRY
delete from t1 where (a = f2(5));
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1006);
--error ER_DUP_ENTRY
delete from t1, t3 using t1, t3 where (f2(6) = 6) ;
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1007);
--error ER_DUP_ENTRY
replace t1 values (f2(7));
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1008);
--error ER_DUP_ENTRY
replace into t3 (a) select f2(8) from t1;
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1009);
--error ER_DUP_ENTRY
select f2(9) from t1 ;
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1010);
--error ER_DUP_ENTRY
show databases where (f2(10) = 10);
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1011);
--error ER_DUP_ENTRY
show tables where (f2(11) = 11);
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1012);
--error ER_DUP_ENTRY
show triggers where (f2(12) = 12);
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1013);
--error ER_DUP_ENTRY
show table status where (f2(13) = 13);
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1014);
--error ER_DUP_ENTRY
show open tables where (f2(14) = 14);
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1015);
--error ER_DUP_ENTRY
show columns in mysql.proc where (f2(15) = 15);
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1016);
--error ER_DUP_ENTRY
show status where (f2(16) = 16);
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1017);
--error ER_DUP_ENTRY
show variables where (f2(17) = 17);
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1018);
--error ER_DUP_ENTRY
show charset where (f2(18) = 18);
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1019);
--error ER_DUP_ENTRY
show collation where (f2(19) = 19);
select * from t2;
rollback;
select * from t2;
--echo # We need at least one procedure to make sure the WHERE clause is
--echo # evaluated
create procedure dummy() begin end;
insert into t2 (a) values (1020);
--error ER_DUP_ENTRY
show procedure status where (f2(20) = 20);
select * from t2;
rollback;
select * from t2;
drop procedure dummy;
insert into t2 (a) values (1021);
--error ER_DUP_ENTRY
show function status where (f2(21) = 21);
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1022);
prepare stmt from "insert into t1 (a) values (f2(22))";
--error ER_DUP_ENTRY
execute stmt;
select * from t2;
rollback;
select * from t2;
insert into t2 (a) values (1023);
do (f2(23));
select * from t2;
rollback;
select * from t2;
## Please note :
## This will insert a record 1024 in t1 (statement commit)
## This will insert a record 24 in t1 (statement commit)
## then will rollback the second insert only (24) (statement rollback)
## then will rollback the complete transaction (transaction rollback)
delimiter |;
create procedure bug12713_call ()
begin
insert into t2 (a) values (24);
insert into t2 (a) values (24);
end|
delimiter ;|
insert into t2 (a) values (1024);
--error ER_DUP_ENTRY
call bug12713_call();
select * from t2;
rollback;
select * from t2;
--echo =======================================================================
--echo Testing select_to_file
--echo =======================================================================
insert into t2 (a) values (1025);
--replace_result $MYSQLTEST_VARDIR ..
--error ER_DUP_ENTRY
eval select f2(25) into outfile "$MYSQLTEST_VARDIR/tmp/dml.out" from t1;
select * from t2;
rollback;
select * from t2;
--remove_file $MYSQLTEST_VARDIR/tmp/dml.out
insert into t2 (a) values (1026);
--replace_result $MYSQLTEST_VARDIR ..
--error ER_DUP_ENTRY
eval load data infile "../std_data_ln/words.dat" into table t1 (a) set a:=f2(26);
select * from t2;
rollback;
select * from t2;
--echo =======================================================================
--echo Testing select_dumpvar
--echo =======================================================================
insert into t2 (a) values (1027);
--error ER_DUP_ENTRY
select f2(27) into @foo;
select * from t2;
rollback;
select * from t2;
--echo =======================================================================
--echo Testing Select_fetch_into_spvars
--echo =======================================================================
delimiter |;
create procedure bug12713_dump_spvars ()
begin
declare foo int;
declare continue handler for sqlexception
begin
select "Exception trapped";
end;
select f2(28) into foo;
select * from t2;
end|
delimiter ;|
insert into t2 (a) values (1028);
call bug12713_dump_spvars ();
rollback;
select * from t2;
--echo =======================================================================
--echo Cleanup
--echo =======================================================================
set autocommit=default;
drop table t1;
drop table t2;
drop table t3;
drop function f2;
drop procedure bug12713_call;
drop procedure bug12713_dump_spvars;
--echo #
--echo # Bug#12713 Error in a stored function called from a SELECT doesn't
--echo # cause ROLLBACK of statem
--echo #
--echo # Verify that two-phase commit is not issued for read-only
--echo # transactions.
--echo #
--echo # Verify that two-phase commit is issued for read-write transactions,
--echo # even if the change is done inside a stored function called from
--echo # SELECT or SHOW statement.
--echo #
set autocommit=0;
--disable_warnings
drop table if exists t1;
drop table if exists t2;
drop function if exists f1;
drop procedure if exists p_verify_status_increment;
--enable_warnings
set sql_mode=no_engine_substitution;
create table t1 (a int unique);
create table t2 (a int) engine=myisam;
set sql_mode=default;
--echo #
--echo # An auxiliary procedure to track Handler_prepare and Handler_commit
--echo # statistics.
--echo #
delimiter |;
create procedure
p_verify_status_increment(commit_inc_mixed int, prepare_inc_mixed int,
commit_inc_row int, prepare_inc_row int)
begin
declare commit_inc int;
declare prepare_inc int;
declare old_commit_count int default ifnull(@commit_count, 0);
declare old_prepare_count int default ifnull(@prepare_count, 0);
declare c_res int;
# Use a cursor to have just one access to I_S instead of 2, it is very slow
# and amounts for over 90% of test CPU time
declare c cursor for
select variable_value
from information_schema.session_status
where variable_name='Handler_commit' or variable_name='Handler_prepare'
order by variable_name;
if @@global.binlog_format = 'ROW' then
set commit_inc= commit_inc_row;
set prepare_inc= prepare_inc_row;
else
set commit_inc= commit_inc_mixed;
set prepare_inc= prepare_inc_mixed;
end if;
open c;
fetch c into c_res;
set @commit_count=c_res;
fetch c into c_res;
set @prepare_count=c_res;
close c;
if old_commit_count + commit_inc <> @commit_count then
select concat("Expected commit increment: ", commit_inc,
" actual: ", @commit_count - old_commit_count)
as 'ERROR';
elseif old_prepare_count + prepare_inc <> @prepare_count then
select concat("Expected prepare increment: ", prepare_inc,
" actual: ", @prepare_count - old_prepare_count)
as 'ERROR';
else
select '' as 'SUCCESS';
end if;
end|
delimiter ;|
--echo # Reset Handler_commit and Handler_prepare counters
flush status;
--echo #
--echo # 1. Read-only statement: SELECT
--echo #
select * from t1;
call p_verify_status_increment(1, 0, 1, 0);
commit;
call p_verify_status_increment(1, 0, 1, 0);
--echo # 2. Read-write statement: INSERT, insert 1 row.
--echo #
insert into t1 (a) values (1);
call p_verify_status_increment(2, 2, 2, 2);
commit;
call p_verify_status_increment(2, 2, 2, 2);
--echo # 3. Read-write statement: UPDATE, update 1 row.
--echo #
update t1 set a=2;
call p_verify_status_increment(2, 2, 2, 2);
commit;
call p_verify_status_increment(2, 2, 2, 2);
--echo # 4. Read-write statement: UPDATE, update 0 rows, 1 row matches WHERE
--echo #
--echo # Note the wrong Handler_prepare/Handler_commit count is due to
--echo # Bug#29157 "UPDATE, changed rows incorrect" and
--echo # Bug#Bug #33846 UPDATE word:Wrong 'Changed rows' if InnoDB, unique
--echo # key and no rows qualify WHERE
--echo #
update t1 set a=2;
call p_verify_status_increment(2, 2, 2, 2);
commit;
call p_verify_status_increment(2, 2, 2, 2);
--echo # 5. Read-write statement: UPDATE, update 0 rows, 0 rows match WHERE
--echo #
--echo # In mixed replication mode, there is a read-only transaction
--echo # in InnoDB and also the statement is written to the binary log.
--echo # So we have two commits but no 2pc, since the first engine's
--echo # transaction is read-only.
--echo # In the row level replication mode, we only have the read-only
--echo # transaction in InnoDB and nothing is written to the binary log.
--echo #
update t1 set a=3 where a=1;
call p_verify_status_increment(2, 0, 1, 0);
commit;
call p_verify_status_increment(2, 0, 1, 0);
--echo # 6. Read-write statement: DELETE, delete 0 rows.
--echo #
delete from t1 where a=1;
call p_verify_status_increment(2, 0, 1, 0);
commit;
call p_verify_status_increment(2, 0, 1, 0);
--echo # 7. Read-write statement: DELETE, delete 1 row.
--echo #
delete from t1 where a=2;
call p_verify_status_increment(2, 2, 2, 2);
commit;
call p_verify_status_increment(2, 2, 2, 2);
--echo # 8. Read-write statement: unqualified DELETE
--echo #
--echo # In statement or mixed replication mode, we call
--echo # handler::ha_delete_all_rows() and write statement text
--echo # to the binary log. This results in two read-write transactions.
--echo # In row level replication mode, we do not call
--echo # handler::ha_delete_all_rows(), but delete rows one by one.
--echo # Since there are no rows, nothing is written to the binary log.
--echo # Thus we have just one read-only transaction in InnoDB.
delete from t1;
call p_verify_status_increment(2, 2, 1, 0);
commit;
call p_verify_status_increment(2, 2, 1, 0);
--echo # 9. Read-write statement: REPLACE, change 1 row.
--echo #
replace t1 set a=1;
call p_verify_status_increment(2, 2, 2, 2);
commit;
call p_verify_status_increment(2, 2, 2, 2);
--echo # 10. Read-write statement: REPLACE, change 0 rows.
--echo #
replace t1 set a=1;
call p_verify_status_increment(2, 2, 2, 2);
commit;
call p_verify_status_increment(2, 2, 2, 2);
--echo # 11. Read-write statement: IODKU, change 1 row.
--echo #
insert t1 set a=1 on duplicate key update a=a+1;
call p_verify_status_increment(2, 2, 2, 2);
select * from t1;
call p_verify_status_increment(1, 0, 1, 0);
commit;
call p_verify_status_increment(2, 2, 2, 2);
--echo # 12. Read-write statement: IODKU, change 0 rows.
--echo #
insert t1 set a=2 on duplicate key update a=2;
call p_verify_status_increment(1, 0, 1, 0);
commit;
call p_verify_status_increment(1, 0, 1, 0);
--echo # 13. Read-write statement: INSERT IGNORE, change 0 rows.
--echo #
insert ignore t1 set a=2;
call p_verify_status_increment(1, 0, 1, 0);
commit;
call p_verify_status_increment(1, 0, 1, 0);
--echo # 14. Read-write statement: INSERT IGNORE, change 1 row.
--echo #
insert ignore t1 set a=1;
call p_verify_status_increment(2, 2, 2, 2);
commit;
call p_verify_status_increment(2, 2, 2, 2);
--echo # 15. Read-write statement: UPDATE IGNORE, change 0 rows.
--echo #
update ignore t1 set a=2 where a=1;
call p_verify_status_increment(2, 2, 1, 0);
commit;
call p_verify_status_increment(2, 2, 1, 0);
--echo #
--echo # Create a stored function that modifies a
--echo # non-transactional table. Demonstrate that changes in
--echo # non-transactional tables do not affect the two phase commit
--echo # algorithm.
--echo #
delimiter |;
create function f1() returns int
begin
insert t2 set a=2;
return 2;
end|
delimiter ;|
call p_verify_status_increment(0, 0, 0, 0);
--echo # 16. A function changes non-trans-table.
--echo #
select f1();
call p_verify_status_increment(0, 0, 0, 0);
commit;
call p_verify_status_increment(0, 0, 0, 0);
--echo # 17. Read-only statement, a function changes non-trans-table.
--echo #
select f1() from t1;
call p_verify_status_increment(1, 0, 1, 0);
commit;
call p_verify_status_increment(1, 0, 1, 0);
--echo # 18. Read-write statement: UPDATE, change 0 (transactional) rows.
--echo #
select count(*) from t2;
update t1 set a=2 where a=f1()+10;
select count(*) from t2;
call p_verify_status_increment(2, 0, 2, 0);
commit;
call p_verify_status_increment(2, 0, 2, 0);
--echo #
--echo # Replace the non-transactional table with a temporary
--echo # transactional table. Demonstrate that a change to a temporary
--echo # transactional table does not provoke 2-phase commit, although
--echo # does trigger a commit and a binlog write (in statement mode).
--echo #
drop table t2;
set sql_mode=no_engine_substitution;
create temporary table t2 (a int);
call p_verify_status_increment(0, 0, 0, 0);
set sql_mode=default;
--echo # 19. A function changes temp-trans-table.
--echo #
select f1();
--echo # Two commits because a binary log record is written
call p_verify_status_increment(2, 0, 1, 0);
commit;
call p_verify_status_increment(2, 0, 1, 0);
--echo # 20. Read-only statement, a function changes non-trans-table.
--echo #
select f1() from t1;
--echo # Two commits because a binary log record is written
call p_verify_status_increment(2, 0, 1, 0);
commit;
call p_verify_status_increment(2, 0, 1, 0);
--echo # 21. Read-write statement: UPDATE, change 0 (transactional) rows.
--echo #
update t1 set a=2 where a=f1()+10;
call p_verify_status_increment(2, 0, 1, 0);
commit;
call p_verify_status_increment(2, 0, 1, 0);
--echo # 22. DDL: ALTER TEMPORARY TABLE, should not cause a 2pc
--echo #
alter table t2 add column b int default 5;
--echo # A commit is done internally by ALTER.
call p_verify_status_increment(2, 0, 2, 0);
commit;
--echo # There is nothing left to commit
call p_verify_status_increment(0, 0, 0, 0);
--echo # 23. DDL: RENAME TEMPORARY TABLE, does not start a transaction
--echo
--echo # No test because of Bug#8729 "rename table fails on temporary table"
--echo # 24. DDL: TRUNCATE TEMPORARY TABLE, does not start a transaction
--echo
truncate table t2;
call p_verify_status_increment(2, 0, 2, 0);
commit;
--echo # There is nothing left to commit
call p_verify_status_increment(0, 0, 0, 0);
--echo # 25. Read-write statement: unqualified DELETE
--echo
delete from t2;
call p_verify_status_increment(2, 0, 1, 0);
commit;
--echo # There is nothing left to commit
call p_verify_status_increment(2, 0, 1, 0);
--echo # 25. DDL: DROP TEMPORARY TABLE, does not start a transaction
--echo #
drop temporary table t2;
call p_verify_status_increment(0, 0, 0, 0);
commit;
call p_verify_status_increment(0, 0, 0, 0);
--echo # 26. Verify that SET AUTOCOMMIT issues an implicit commit
--echo #
insert t1 set a=3;
call p_verify_status_increment(2, 2, 2, 2);
set autocommit=1;
call p_verify_status_increment(2, 2, 2, 2);
rollback;
select a from t1 where a=3;
call p_verify_status_increment(1, 0, 1, 0);
delete from t1 where a=3;
call p_verify_status_increment(2, 2, 2, 2);
commit;
call p_verify_status_increment(0, 0, 0, 0);
set autocommit=0;
call p_verify_status_increment(0, 0, 0, 0);
insert t1 set a=3;
call p_verify_status_increment(2, 2, 2, 2);
--echo # Sic: not actually changing the value of autocommit
set autocommit=0;
call p_verify_status_increment(0, 0, 0, 0);
rollback;
select a from t1 where a=3;
call p_verify_status_increment(1, 0, 1, 0);
--echo # 27. Savepoint management
--echo #
insert t1 set a=3;
call p_verify_status_increment(2, 2, 2, 2);
savepoint a;
call p_verify_status_increment(0, 0, 0, 0);
insert t1 set a=4;
--echo # Sic: a bug. Binlog did not register itself this time.
call p_verify_status_increment(1, 0, 1, 0);
release savepoint a;
rollback;
call p_verify_status_increment(0, 0, 0, 0);
select a from t1 where a=3;
call p_verify_status_increment(1, 0, 1, 0);
commit;
call p_verify_status_increment(1, 0, 1, 0);
--echo # 28. Read-write statement: DO
--echo #
create table t2 (a int);
call p_verify_status_increment(0, 0, 0, 0);
do (select f1() from t1 where a=2);
call p_verify_status_increment(2, 2, 2, 2);
commit;
call p_verify_status_increment(2, 2, 2, 2);
--echo # 29. Read-write statement: MULTI-DELETE
--echo #
delete t1, t2 from t1 join t2 on (t1.a=t2.a) where t1.a=2;
commit;
call p_verify_status_increment(4, 4, 4, 4);
--echo # 30. Read-write statement: INSERT-SELECT, MULTI-UPDATE, REPLACE-SELECT
--echo #
insert into t2 select a from t1;
commit;
replace into t2 select a from t1;
commit;
call p_verify_status_increment(8, 8, 8, 8);
#
# Multi-update is one of the few remaining statements that still
# locks the tables at prepare step (and hence starts the transaction.
# Disable the PS protocol, since in this protocol we get a different
# number of commmits (there is an extra commit after prepare
#
--disable_ps_protocol
update t1, t2 set t1.a=4, t2.a=8 where t1.a=t2.a and t1.a=1;
--enable_ps_protocol
commit;
call p_verify_status_increment(4, 4, 4, 4);
--echo # 31. DDL: various DDL with transactional tables
--echo #
--echo # Sic: no table is created.
create table if not exists t2 (a int) select 6 union select 7;
--echo # Sic: first commits the statement, and then the transaction.
call p_verify_status_increment(4, 4, 4, 4);
create table t3 select a from t2;
call p_verify_status_increment(4, 4, 4, 4);
alter table t3 add column (b int);
call p_verify_status_increment(2, 0, 2, 0);
alter table t3 rename t4;
call p_verify_status_increment(1, 0, 1, 0);
rename table t4 to t3;
call p_verify_status_increment(1, 0, 1, 0);
truncate table t3;
call p_verify_status_increment(2, 2, 2, 2);
create view v1 as select * from t2;
call p_verify_status_increment(1, 0, 1, 0);
check table t1;
call p_verify_status_increment(3, 0, 3, 0);
--echo # Sic: after this bug is fixed, CHECK leaves no pending transaction
commit;
call p_verify_status_increment(0, 0, 0, 0);
check table t1, t2, t3;
call p_verify_status_increment(6, 0, 6, 0);
commit;
call p_verify_status_increment(0, 0, 0, 0);
drop view v1;
call p_verify_status_increment(0, 0, 0, 0);
--echo #
--echo # Cleanup
--echo #
drop table t1;
drop procedure p_verify_status_increment;
drop function f1;

View File

@ -0,0 +1,888 @@
set sql_mode=no_engine_substitution;
set storage_engine = InnoDB;
set autocommit=1;
drop table if exists t1;
drop table if exists t2;
drop table if exists t3;
drop function if exists f2;
drop procedure if exists bug12713_call;
drop procedure if exists bug12713_dump_spvars;
drop procedure if exists dummy;
create table t1 (a int);
create table t2 (a int unique);
create table t3 (a int);
set sql_mode=default;
insert into t1 (a) values (1), (2);
insert into t3 (a) values (1), (2);
create function f2(x int) returns int
begin
insert into t2 (a) values (x);
insert into t2 (a) values (x);
return x;
end|
set autocommit=0;
flush status;
insert into t2 (a) values (1001);
insert into t1 (a) values (f2(1));
ERROR 23000: Duplicate entry '1' for key 'a'
select * from t2;
a
1001
rollback;
select * from t2;
a
insert into t2 (a) values (1002);
insert into t3 (a) select f2(2) from t1;
ERROR 23000: Duplicate entry '2' for key 'a'
select * from t2;
a
1002
rollback;
select * from t2;
a
insert into t2 (a) values (1003);
update t1 set a= a + f2(3);
ERROR 23000: Duplicate entry '3' for key 'a'
select * from t2;
a
1003
rollback;
select * from t2;
a
insert into t2 (a) values (1004);
update t1, t3 set t1.a = 0, t3.a = 0 where (f2(4) = 4) and (t1.a = t3.a);
ERROR 23000: Duplicate entry '4' for key 'a'
select * from t2;
a
1004
rollback;
select * from t2;
a
insert into t2 (a) values (1005);
delete from t1 where (a = f2(5));
ERROR 23000: Duplicate entry '5' for key 'a'
select * from t2;
a
1005
rollback;
select * from t2;
a
insert into t2 (a) values (1006);
delete from t1, t3 using t1, t3 where (f2(6) = 6) ;
ERROR 23000: Duplicate entry '6' for key 'a'
select * from t2;
a
1006
rollback;
select * from t2;
a
insert into t2 (a) values (1007);
replace t1 values (f2(7));
ERROR 23000: Duplicate entry '7' for key 'a'
select * from t2;
a
1007
rollback;
select * from t2;
a
insert into t2 (a) values (1008);
replace into t3 (a) select f2(8) from t1;
ERROR 23000: Duplicate entry '8' for key 'a'
select * from t2;
a
1008
rollback;
select * from t2;
a
insert into t2 (a) values (1009);
select f2(9) from t1 ;
ERROR 23000: Duplicate entry '9' for key 'a'
select * from t2;
a
1009
rollback;
select * from t2;
a
insert into t2 (a) values (1010);
show databases where (f2(10) = 10);
ERROR 23000: Duplicate entry '10' for key 'a'
select * from t2;
a
1010
rollback;
select * from t2;
a
insert into t2 (a) values (1011);
show tables where (f2(11) = 11);
ERROR 23000: Duplicate entry '11' for key 'a'
select * from t2;
a
1011
rollback;
select * from t2;
a
insert into t2 (a) values (1012);
show triggers where (f2(12) = 12);
ERROR 23000: Duplicate entry '12' for key 'a'
select * from t2;
a
1012
rollback;
select * from t2;
a
insert into t2 (a) values (1013);
show table status where (f2(13) = 13);
ERROR 23000: Duplicate entry '13' for key 'a'
select * from t2;
a
1013
rollback;
select * from t2;
a
insert into t2 (a) values (1014);
show open tables where (f2(14) = 14);
ERROR 23000: Duplicate entry '14' for key 'a'
select * from t2;
a
1014
rollback;
select * from t2;
a
insert into t2 (a) values (1015);
show columns in mysql.proc where (f2(15) = 15);
ERROR 23000: Duplicate entry '15' for key 'a'
select * from t2;
a
1015
rollback;
select * from t2;
a
insert into t2 (a) values (1016);
show status where (f2(16) = 16);
ERROR 23000: Duplicate entry '16' for key 'a'
select * from t2;
a
1016
rollback;
select * from t2;
a
insert into t2 (a) values (1017);
show variables where (f2(17) = 17);
ERROR 23000: Duplicate entry '17' for key 'a'
select * from t2;
a
1017
rollback;
select * from t2;
a
insert into t2 (a) values (1018);
show charset where (f2(18) = 18);
ERROR 23000: Duplicate entry '18' for key 'a'
select * from t2;
a
1018
rollback;
select * from t2;
a
insert into t2 (a) values (1019);
show collation where (f2(19) = 19);
ERROR 23000: Duplicate entry '19' for key 'a'
select * from t2;
a
1019
rollback;
select * from t2;
a
# We need at least one procedure to make sure the WHERE clause is
# evaluated
create procedure dummy() begin end;
insert into t2 (a) values (1020);
show procedure status where (f2(20) = 20);
ERROR 23000: Duplicate entry '20' for key 'a'
select * from t2;
a
1020
rollback;
select * from t2;
a
drop procedure dummy;
insert into t2 (a) values (1021);
show function status where (f2(21) = 21);
ERROR 23000: Duplicate entry '21' for key 'a'
select * from t2;
a
1021
rollback;
select * from t2;
a
insert into t2 (a) values (1022);
prepare stmt from "insert into t1 (a) values (f2(22))";
execute stmt;
ERROR 23000: Duplicate entry '22' for key 'a'
select * from t2;
a
1022
rollback;
select * from t2;
a
insert into t2 (a) values (1023);
do (f2(23));
Warnings:
Error 1062 Duplicate entry '23' for key 'a'
select * from t2;
a
1023
rollback;
select * from t2;
a
create procedure bug12713_call ()
begin
insert into t2 (a) values (24);
insert into t2 (a) values (24);
end|
insert into t2 (a) values (1024);
call bug12713_call();
ERROR 23000: Duplicate entry '24' for key 'a'
select * from t2;
a
24
1024
rollback;
select * from t2;
a
=======================================================================
Testing select_to_file
=======================================================================
insert into t2 (a) values (1025);
select f2(25) into outfile "../tmp/dml.out" from t1;
ERROR 23000: Duplicate entry '25' for key 'a'
select * from t2;
a
1025
rollback;
select * from t2;
a
insert into t2 (a) values (1026);
load data infile "../std_data_ln/words.dat" into table t1 (a) set a:=f2(26);
ERROR 23000: Duplicate entry '26' for key 'a'
select * from t2;
a
1026
rollback;
select * from t2;
a
=======================================================================
Testing select_dumpvar
=======================================================================
insert into t2 (a) values (1027);
select f2(27) into @foo;
ERROR 23000: Duplicate entry '27' for key 'a'
select * from t2;
a
1027
rollback;
select * from t2;
a
=======================================================================
Testing Select_fetch_into_spvars
=======================================================================
create procedure bug12713_dump_spvars ()
begin
declare foo int;
declare continue handler for sqlexception
begin
select "Exception trapped";
end;
select f2(28) into foo;
select * from t2;
end|
insert into t2 (a) values (1028);
call bug12713_dump_spvars ();
Exception trapped
Exception trapped
a
1028
rollback;
select * from t2;
a
=======================================================================
Cleanup
=======================================================================
set autocommit=default;
drop table t1;
drop table t2;
drop table t3;
drop function f2;
drop procedure bug12713_call;
drop procedure bug12713_dump_spvars;
#
# Bug#12713 Error in a stored function called from a SELECT doesn't
# cause ROLLBACK of statem
#
# Verify that two-phase commit is not issued for read-only
# transactions.
#
# Verify that two-phase commit is issued for read-write transactions,
# even if the change is done inside a stored function called from
# SELECT or SHOW statement.
#
set autocommit=0;
drop table if exists t1;
drop table if exists t2;
drop function if exists f1;
drop procedure if exists p_verify_status_increment;
set sql_mode=no_engine_substitution;
create table t1 (a int unique);
create table t2 (a int) engine=myisam;
set sql_mode=default;
#
# An auxiliary procedure to track Handler_prepare and Handler_commit
# statistics.
#
create procedure
p_verify_status_increment(commit_inc_mixed int, prepare_inc_mixed int,
commit_inc_row int, prepare_inc_row int)
begin
declare commit_inc int;
declare prepare_inc int;
declare old_commit_count int default ifnull(@commit_count, 0);
declare old_prepare_count int default ifnull(@prepare_count, 0);
declare c_res int;
# Use a cursor to have just one access to I_S instead of 2, it is very slow
# and amounts for over 90% of test CPU time
declare c cursor for
select variable_value
from information_schema.session_status
where variable_name='Handler_commit' or variable_name='Handler_prepare'
order by variable_name;
if @@global.binlog_format = 'ROW' then
set commit_inc= commit_inc_row;
set prepare_inc= prepare_inc_row;
else
set commit_inc= commit_inc_mixed;
set prepare_inc= prepare_inc_mixed;
end if;
open c;
fetch c into c_res;
set @commit_count=c_res;
fetch c into c_res;
set @prepare_count=c_res;
close c;
if old_commit_count + commit_inc <> @commit_count then
select concat("Expected commit increment: ", commit_inc,
" actual: ", @commit_count - old_commit_count)
as 'ERROR';
elseif old_prepare_count + prepare_inc <> @prepare_count then
select concat("Expected prepare increment: ", prepare_inc,
" actual: ", @prepare_count - old_prepare_count)
as 'ERROR';
else
select '' as 'SUCCESS';
end if;
end|
# Reset Handler_commit and Handler_prepare counters
flush status;
#
# 1. Read-only statement: SELECT
#
select * from t1;
a
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
commit;
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
# 2. Read-write statement: INSERT, insert 1 row.
#
insert into t1 (a) values (1);
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
commit;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
# 3. Read-write statement: UPDATE, update 1 row.
#
update t1 set a=2;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
commit;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
# 4. Read-write statement: UPDATE, update 0 rows, 1 row matches WHERE
#
# Note the wrong Handler_prepare/Handler_commit count is due to
# Bug#29157 "UPDATE, changed rows incorrect" and
# Bug#Bug #33846 UPDATE word:Wrong 'Changed rows' if InnoDB, unique
# key and no rows qualify WHERE
#
update t1 set a=2;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
commit;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
# 5. Read-write statement: UPDATE, update 0 rows, 0 rows match WHERE
#
# In mixed replication mode, there is a read-only transaction
# in InnoDB and also the statement is written to the binary log.
# So we have two commits but no 2pc, since the first engine's
# transaction is read-only.
# In the row level replication mode, we only have the read-only
# transaction in InnoDB and nothing is written to the binary log.
#
update t1 set a=3 where a=1;
call p_verify_status_increment(2, 0, 1, 0);
SUCCESS
commit;
call p_verify_status_increment(2, 0, 1, 0);
SUCCESS
# 6. Read-write statement: DELETE, delete 0 rows.
#
delete from t1 where a=1;
call p_verify_status_increment(2, 0, 1, 0);
SUCCESS
commit;
call p_verify_status_increment(2, 0, 1, 0);
SUCCESS
# 7. Read-write statement: DELETE, delete 1 row.
#
delete from t1 where a=2;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
commit;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
# 8. Read-write statement: unqualified DELETE
#
# In statement or mixed replication mode, we call
# handler::ha_delete_all_rows() and write statement text
# to the binary log. This results in two read-write transactions.
# In row level replication mode, we do not call
# handler::ha_delete_all_rows(), but delete rows one by one.
# Since there are no rows, nothing is written to the binary log.
# Thus we have just one read-only transaction in InnoDB.
delete from t1;
call p_verify_status_increment(2, 2, 1, 0);
SUCCESS
commit;
call p_verify_status_increment(2, 2, 1, 0);
SUCCESS
# 9. Read-write statement: REPLACE, change 1 row.
#
replace t1 set a=1;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
commit;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
# 10. Read-write statement: REPLACE, change 0 rows.
#
replace t1 set a=1;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
commit;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
# 11. Read-write statement: IODKU, change 1 row.
#
insert t1 set a=1 on duplicate key update a=a+1;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
select * from t1;
a
2
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
commit;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
# 12. Read-write statement: IODKU, change 0 rows.
#
insert t1 set a=2 on duplicate key update a=2;
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
commit;
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
# 13. Read-write statement: INSERT IGNORE, change 0 rows.
#
insert ignore t1 set a=2;
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
commit;
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
# 14. Read-write statement: INSERT IGNORE, change 1 row.
#
insert ignore t1 set a=1;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
commit;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
# 15. Read-write statement: UPDATE IGNORE, change 0 rows.
#
update ignore t1 set a=2 where a=1;
call p_verify_status_increment(2, 2, 1, 0);
SUCCESS
commit;
call p_verify_status_increment(2, 2, 1, 0);
SUCCESS
#
# Create a stored function that modifies a
# non-transactional table. Demonstrate that changes in
# non-transactional tables do not affect the two phase commit
# algorithm.
#
create function f1() returns int
begin
insert t2 set a=2;
return 2;
end|
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
# 16. A function changes non-trans-table.
#
select f1();
f1()
2
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
commit;
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
# 17. Read-only statement, a function changes non-trans-table.
#
select f1() from t1;
f1()
2
2
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
commit;
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
# 18. Read-write statement: UPDATE, change 0 (transactional) rows.
#
select count(*) from t2;
count(*)
3
update t1 set a=2 where a=f1()+10;
select count(*) from t2;
count(*)
5
call p_verify_status_increment(2, 0, 2, 0);
SUCCESS
commit;
call p_verify_status_increment(2, 0, 2, 0);
SUCCESS
#
# Replace the non-transactional table with a temporary
# transactional table. Demonstrate that a change to a temporary
# transactional table does not provoke 2-phase commit, although
# does trigger a commit and a binlog write (in statement mode).
#
drop table t2;
set sql_mode=no_engine_substitution;
create temporary table t2 (a int);
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
set sql_mode=default;
# 19. A function changes temp-trans-table.
#
select f1();
f1()
2
# Two commits because a binary log record is written
call p_verify_status_increment(2, 0, 1, 0);
SUCCESS
commit;
call p_verify_status_increment(2, 0, 1, 0);
SUCCESS
# 20. Read-only statement, a function changes non-trans-table.
#
select f1() from t1;
f1()
2
2
# Two commits because a binary log record is written
call p_verify_status_increment(2, 0, 1, 0);
SUCCESS
commit;
call p_verify_status_increment(2, 0, 1, 0);
SUCCESS
# 21. Read-write statement: UPDATE, change 0 (transactional) rows.
#
update t1 set a=2 where a=f1()+10;
call p_verify_status_increment(2, 0, 1, 0);
SUCCESS
commit;
call p_verify_status_increment(2, 0, 1, 0);
SUCCESS
# 22. DDL: ALTER TEMPORARY TABLE, should not cause a 2pc
#
alter table t2 add column b int default 5;
# A commit is done internally by ALTER.
call p_verify_status_increment(2, 0, 2, 0);
SUCCESS
commit;
# There is nothing left to commit
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
# 23. DDL: RENAME TEMPORARY TABLE, does not start a transaction
# No test because of Bug#8729 "rename table fails on temporary table"
# 24. DDL: TRUNCATE TEMPORARY TABLE, does not start a transaction
truncate table t2;
call p_verify_status_increment(2, 0, 2, 0);
SUCCESS
commit;
# There is nothing left to commit
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
# 25. Read-write statement: unqualified DELETE
delete from t2;
call p_verify_status_increment(2, 0, 1, 0);
SUCCESS
commit;
# There is nothing left to commit
call p_verify_status_increment(2, 0, 1, 0);
SUCCESS
# 25. DDL: DROP TEMPORARY TABLE, does not start a transaction
#
drop temporary table t2;
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
commit;
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
# 26. Verify that SET AUTOCOMMIT issues an implicit commit
#
insert t1 set a=3;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
set autocommit=1;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
rollback;
select a from t1 where a=3;
a
3
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
delete from t1 where a=3;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
commit;
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
set autocommit=0;
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
insert t1 set a=3;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
# Sic: not actually changing the value of autocommit
set autocommit=0;
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
rollback;
select a from t1 where a=3;
a
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
# 27. Savepoint management
#
insert t1 set a=3;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
savepoint a;
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
insert t1 set a=4;
# Sic: a bug. Binlog did not register itself this time.
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
release savepoint a;
rollback;
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
select a from t1 where a=3;
a
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
commit;
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
# 28. Read-write statement: DO
#
create table t2 (a int);
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
do (select f1() from t1 where a=2);
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
commit;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
# 29. Read-write statement: MULTI-DELETE
#
delete t1, t2 from t1 join t2 on (t1.a=t2.a) where t1.a=2;
commit;
call p_verify_status_increment(4, 4, 4, 4);
SUCCESS
# 30. Read-write statement: INSERT-SELECT, MULTI-UPDATE, REPLACE-SELECT
#
insert into t2 select a from t1;
commit;
replace into t2 select a from t1;
commit;
call p_verify_status_increment(8, 8, 8, 8);
SUCCESS
update t1, t2 set t1.a=4, t2.a=8 where t1.a=t2.a and t1.a=1;
commit;
call p_verify_status_increment(4, 4, 4, 4);
SUCCESS
# 31. DDL: various DDL with transactional tables
#
# Sic: no table is created.
create table if not exists t2 (a int) select 6 union select 7;
Warnings:
Note 1050 Table 't2' already exists
# Sic: first commits the statement, and then the transaction.
call p_verify_status_increment(4, 4, 4, 4);
SUCCESS
create table t3 select a from t2;
call p_verify_status_increment(4, 4, 4, 4);
SUCCESS
alter table t3 add column (b int);
call p_verify_status_increment(2, 0, 2, 0);
SUCCESS
alter table t3 rename t4;
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
rename table t4 to t3;
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
truncate table t3;
call p_verify_status_increment(2, 2, 2, 2);
SUCCESS
create view v1 as select * from t2;
call p_verify_status_increment(1, 0, 1, 0);
SUCCESS
check table t1;
Table Op Msg_type Msg_text
test.t1 check status OK
call p_verify_status_increment(3, 0, 3, 0);
SUCCESS
# Sic: after this bug is fixed, CHECK leaves no pending transaction
commit;
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
check table t1, t2, t3;
Table Op Msg_type Msg_text
test.t1 check status OK
test.t2 check status OK
test.t3 check status OK
call p_verify_status_increment(6, 0, 6, 0);
SUCCESS
commit;
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
drop view v1;
call p_verify_status_increment(0, 0, 0, 0);
SUCCESS
#
# Cleanup
#
drop table t1;
drop procedure p_verify_status_increment;
drop function f1;

View File

@ -373,7 +373,7 @@ master-bin.000001 # Write_rows # # table_id: # flags: STMT_END_F
master-bin.000001 # Query # # use `test`; BEGIN
master-bin.000001 # Table_map # # table_id: # (test.t2)
master-bin.000001 # Write_rows # # table_id: # flags: STMT_END_F
master-bin.000001 # Xid # # COMMIT /* XID */
master-bin.000001 # Query # # use `test`; COMMIT
master-bin.000001 # Query # # use `test`; DROP TABLE t2
master-bin.000001 # Table_map # # table_id: # (test.t1)
master-bin.000001 # Write_rows # # table_id: # flags: STMT_END_F
@ -384,7 +384,6 @@ master-bin.000001 # Write_rows # # table_id: # flags: STMT_END_F
master-bin.000001 # Table_map # # table_id: # (test.t1)
master-bin.000001 # Write_rows # # table_id: # flags: STMT_END_F
master-bin.000001 # Query # # use `test`; TRUNCATE table t2
master-bin.000001 # Xid # # COMMIT /* XID */
master-bin.000001 # Table_map # # table_id: # (test.t1)
master-bin.000001 # Write_rows # # table_id: # flags: STMT_END_F
master-bin.000001 # Query # # use `test`; DROP TABLE `t1` /* generated by server */

View File

@ -244,7 +244,6 @@ master-bin.000001 # Query # # use `test`; insert into t2 values (20)
master-bin.000001 # Query # # use `test`; drop table t1,t2
master-bin.000001 # Query # # use `test`; create temporary table ti (a int) engine=innodb
master-bin.000001 # Query # # use `test`; insert into ti values(1)
master-bin.000001 # Xid # # COMMIT /* XID */
master-bin.000001 # Query # # use `test`; create temporary table t1 (a int) engine=myisam
master-bin.000001 # Query # # use `test`; insert t1 values (1)
master-bin.000001 # Query # # use `test`; create table t0 (n int)
@ -349,11 +348,10 @@ master-bin.000001 # Query # # use `test`; INSERT INTO t1 values (7,7)
master-bin.000001 # Query # # use `test`; INSERT INTO t1 values (8,8)
master-bin.000001 # Query # # use `test`; INSERT INTO t1 values (9,9)
master-bin.000001 # Query # # use `test`; TRUNCATE table t2
master-bin.000001 # Xid # # COMMIT /* XID */
master-bin.000001 # Query # # use `test`; INSERT INTO t1 values (10,10)
master-bin.000001 # Query # # use `test`; BEGIN
master-bin.000001 # Query # # use `test`; INSERT INTO t2 values (100,100)
master-bin.000001 # Xid # # COMMIT /* XID */
master-bin.000001 # Query # # use `test`; COMMIT
master-bin.000001 # Query # # use `test`; DROP TABLE t1,t2
reset master;
create table t1 (a int) engine=innodb;

View File

@ -21,3 +21,5 @@ rpl_ndb_mix_innodb : Bug #32720 Test rpl_ndb_mix_innodb fails on SPARC a
# the below testcase have been reworked to avoid the bug, test contains comment, keep bug open
#rpl_ndb_dd_advance : Bug#25913 rpl_ndb_dd_advance fails randomly
rpl_ndb_circular : Bug#33849 COMMIT event missing in cluster circular replication.
rpl_ndb_circular_simplex : Bug#33849 COMMIT event missing in cluster circular replication.

View File

@ -0,0 +1,6 @@
-- source include/have_log_bin.inc
-- source include/have_innodb.inc
let $engine_type = InnoDB;
-- source include/commit.inc

View File

@ -283,6 +283,7 @@ static void run_query(THD *thd, char *buf, char *end,
thd_ndb->m_error_code,
(int) thd->is_error(), thd->is_slave_error);
}
close_thread_tables(thd);
/*
XXX: this code is broken. mysql_parse()/mysql_reset_thd_for_next_command()
can not be called from within a statement, and

View File

@ -575,6 +575,295 @@ void ha_close_connection(THD* thd)
/* ========================================================================
======================= TRANSACTIONS ===================================*/
/**
Transaction handling in the server
==================================
In each client connection, MySQL maintains two transactional
states:
- a statement transaction,
- a standard, also called normal transaction.
Historical note
---------------
"Statement transaction" is a non-standard term that comes
from the times when MySQL supported BerkeleyDB storage engine.
First of all, it should be said that in BerkeleyDB auto-commit
mode auto-commits operations that are atomic to the storage
engine itself, such as a write of a record, and are too
high-granular to be atomic from the application perspective
(MySQL). One SQL statement could involve many BerkeleyDB
auto-committed operations and thus BerkeleyDB auto-commit was of
little use to MySQL.
Secondly, instead of SQL standard savepoints, BerkeleyDB
provided the concept of "nested transactions". In a nutshell,
transactions could be arbitrarily nested, but when the parent
transaction was committed or aborted, all its child (nested)
transactions were handled committed or aborted as well.
Commit of a nested transaction, in turn, made its changes
visible, but not durable: it destroyed the nested transaction,
all its changes would become available to the parent and
currently active nested transactions of this parent.
So the mechanism of nested transactions was employed to
provide "all or nothing" guarantee of SQL statements
required by the standard.
A nested transaction would be created at start of each SQL
statement, and destroyed (committed or aborted) at statement
end. Such nested transaction was internally referred to as
a "statement transaction" and gave birth to the term.
<Historical note ends>
Since then a statement transaction is started for each statement
that accesses transactional tables or uses the binary log. If
the statement succeeds, the statement transaction is committed.
If the statement fails, the transaction is rolled back. Commits
of statement transactions are not durable -- each such
transaction is nested in the normal transaction, and if the
normal transaction is rolled back, the effects of all enclosed
statement transactions are undone as well. Technically,
a statement transaction can be viewed as a savepoint which is
maintained automatically in order to make effects of one
statement atomic.
The normal transaction is started by the user and is ended
usually upon a user request as well. The normal transaction
encloses transactions of all statements issued between
its beginning and its end.
In autocommit mode, the normal transaction is equivalent
to the statement transaction.
Since MySQL supports PSEA (pluggable storage engine
architecture), more than one transactional engine can be
active at a time. Hence transactions, from the server
point of view, are always distributed. In particular,
transactional state is maintained independently for each
engine. In order to commit a transaction the two phase
commit protocol is employed.
Not all statements are executed in context of a transaction.
Administrative and status information statements do not modify
engine data, and thus do not start a statement transaction and
also have no effect on the normal transaction. Examples of such
statements are SHOW STATUS and RESET SLAVE.
Similarly DDL statements are not transactional,
and therefore a transaction is [almost] never started for a DDL
statement. The difference between a DDL statement and a purely
administrative statement though is that a DDL statement always
commits the current transaction before proceeding, if there is
any.
At last, SQL statements that work with non-transactional
engines also have no effect on the transaction state of the
connection. Even though they are written to the binary log,
and the binary log is, overall, transactional, the writes
are done in "write-through" mode, directly to the binlog
file, followed with a OS cache sync, in other words,
bypassing the binlog undo log (translog).
They do not commit the current normal transaction.
A failure of a statement that uses non-transactional tables
would cause a rollback of the statement transaction, but
in case there no non-transactional tables are used,
no statement transaction is started.
Data layout
-----------
The server stores its transaction-related data in
thd->transaction. This structure has two members of type
THD_TRANS. These members correspond to the statement and
normal transactions respectively:
- thd->transaction.stmt contains a list of engines
that are participating in the given statement
- thd->transaction.all contains a list of engines that
have participated in any of the statement transactions started
within the context of the normal transaction.
Each element of the list contains a pointer to the storage
engine, engine-specific transactional data, and engine-specific
transaction flags.
In autocommit mode thd->transaction.all is empty.
Instead, data of thd->transaction.stmt is
used to commit/rollback the normal transaction.
The list of registered engines has a few important properties:
- no engine is registered in the list twice
- engines are present in the list a reverse temporal order --
new participants are always added to the beginning of the list.
Transaction life cycle
----------------------
When a new connection is established, thd->transaction
members are initialized to an empty state.
If a statement uses any tables, all affected engines
are registered in the statement engine list. In
non-autocommit mode, the same engines are registered in
the normal transaction list.
At the end of the statement, the server issues a commit
or a roll back for all engines in the statement list.
At this point transaction flags of an engine, if any, are
propagated from the statement list to the list of the normal
transaction.
When commit/rollback is finished, the statement list is
cleared. It will be filled in again by the next statement,
and emptied again at the next statement's end.
The normal transaction is committed in a similar way
(by going over all engines in thd->transaction.all list)
but at different times:
- upon COMMIT SQL statement is issued by the user
- implicitly, by the server, at the beginning of a DDL statement
or SET AUTOCOMMIT={0|1} statement.
The normal transaction can be rolled back as well:
- if the user has requested so, by issuing ROLLBACK SQL
statement
- if one of the storage engines requested a rollback
by setting thd->transaction_rollback_request. This may
happen in case, e.g., when the transaction in the engine was
chosen a victim of the internal deadlock resolution algorithm
and rolled back internally. When such a situation happens, there
is little the server can do and the only option is to rollback
transactions in all other participating engines. In this case
the rollback is accompanied by an error sent to the user.
As follows from the use cases above, the normal transaction
is never committed when there is an outstanding statement
transaction. In most cases there is no conflict, since
commits of the normal transaction are issued by a stand-alone
administrative or DDL statement, thus no outstanding statement
transaction of the previous statement exists. Besides,
all statements that manipulate with the normal transaction
are prohibited in stored functions and triggers, therefore
no conflicting situation can occur in a sub-statement either.
The remaining rare cases when the server explicitly has
to commit the statement transaction prior to committing the normal
one cover error-handling scenarios (see for example
SQLCOM_LOCK_TABLES).
When committing a statement or a normal transaction, the server
either uses the two-phase commit protocol, or issues a commit
in each engine independently. The two-phase commit protocol
is used only if:
- all participating engines support two-phase commit (provide
handlerton::prepare PSEA API call) and
- transactions in at least two engines modify data (i.e. are
not read-only).
Note that the two phase commit is used for
statement transactions, even though they are not durable anyway.
This is done to ensure logical consistency of data in a multiple-
engine transaction.
For example, imagine that some day MySQL supports unique
constraint checks deferred till the end of statement. In such
case a commit in one of the engines may yield ER_DUP_KEY,
and MySQL should be able to gracefully abort statement
transactions of other participants.
After the normal transaction has been committed,
thd->transaction.all list is cleared.
When a connection is closed, the current normal transaction, if
any, is rolled back.
Roles and responsibilities
--------------------------
The server has no way to know that an engine participates in
the statement and a transaction has been started
in it unless the engine says so. Thus, in order to be
a part of a transaction, the engine must "register" itself.
This is done by invoking trans_register_ha() server call.
Normally the engine registers itself whenever handler::external_lock()
is called. trans_register_ha() can be invoked many times: if
an engine is already registered, the call does nothing.
In case autocommit is not set, the engine must register itself
twice -- both in the statement list and in the normal transaction
list.
In which list to register is a parameter of trans_register_ha().
Note, that although the registration interface in itself is
fairly clear, the current usage practice often leads to undesired
effects. E.g. since a call to trans_register_ha() in most engines
is embedded into implementation of handler::external_lock(), some
DDL statements start a transaction (at least from the server
point of view) even though they are not expected to. E.g.
CREATE TABLE does not start a transaction, since
handler::external_lock() is never called during CREATE TABLE. But
CREATE TABLE ... SELECT does, since handler::external_lock() is
called for the table that is being selected from. This has no
practical effects currently, but must be kept in mind
nevertheless.
Once an engine is registered, the server will do the rest
of the work.
During statement execution, whenever any of data-modifying
PSEA API methods is used, e.g. handler::write_row() or
handler::update_row(), the read-write flag is raised in the
statement transaction for the involved engine.
Currently All PSEA calls are "traced", and the data can not be
changed in a way other than issuing a PSEA call. Important:
unless this invariant is preserved the server will not know that
a transaction in a given engine is read-write and will not
involve the two-phase commit protocol!
At the end of a statement, server call
ha_autocommit_or_rollback() is invoked. This call in turn
invokes handlerton::prepare() for every involved engine.
Prepare is followed by a call to handlerton::commit_one_phase()
If a one-phase commit will suffice, handlerton::prepare() is not
invoked and the server only calls handlerton::commit_one_phase().
At statement commit, the statement-related read-write engine
flag is propagated to the corresponding flag in the normal
transaction. When the commit is complete, the list of registered
engines is cleared.
Rollback is handled in a similar fashion.
Additional notes on DDL and the normal transaction.
---------------------------------------------------
DDLs and operations with non-transactional engines
do not "register" in thd->transaction lists, and thus do not
modify the transaction state. Besides, each DDL in
MySQL is prefixed with an implicit normal transaction commit
(a call to end_active_trans()), and thus leaves nothing
to modify.
However, as it has been pointed out with CREATE TABLE .. SELECT,
some DDL statements can start a *new* transaction.
Behaviour of the server in this case is currently badly
defined.
DDL statements use a form of "semantic" logging
to maintain atomicity: if CREATE TABLE .. SELECT failed,
the newly created table is deleted.
In addition, some DDL statements issue interim transaction
commits: e.g. ALTER TABLE issues a commit after data is copied
from the original table to the internal temporary table. Other
statements, e.g. CREATE TABLE ... SELECT do not always commit
after itself.
And finally there is a group of DDL statements such as
RENAME/DROP TABLE that doesn't start a new transaction
and doesn't commit.
This diversity makes it hard to say what will happen if
by chance a stored function is invoked during a DDL --
whether any modifications it makes will be committed or not
is not clear. Fortunately, SQL grammar of few DDLs allows
invocation of a stored function.
A consistent behaviour is perhaps to always commit the normal
transaction after all DDLs, just like the statement transaction
is always committed at the end of all statements.
*/
/**
Register a storage engine for a transaction.
@ -592,7 +881,7 @@ void ha_close_connection(THD* thd)
void trans_register_ha(THD *thd, bool all, handlerton *ht_arg)
{
THD_TRANS *trans;
handlerton **ht;
Ha_trx_info *ha_info;
DBUG_ENTER("trans_register_ha");
DBUG_PRINT("enter",("%s", all ? "all" : "stmt"));
@ -604,12 +893,13 @@ void trans_register_ha(THD *thd, bool all, handlerton *ht_arg)
else
trans= &thd->transaction.stmt;
for (ht=trans->ht; *ht; ht++)
if (*ht == ht_arg)
DBUG_VOID_RETURN; /* already registered, return */
ha_info= thd->ha_data[ht_arg->slot].ha_info + static_cast<unsigned>(all);
if (ha_info->is_started())
DBUG_VOID_RETURN; /* already registered, return */
ha_info->register_ha(trans, ht_arg);
trans->ht[trans->nht++]=ht_arg;
DBUG_ASSERT(*ht == ht_arg);
trans->no_2pc|=(ht_arg->prepare==0);
if (thd->transaction.xid_state.xid.is_null())
thd->transaction.xid_state.xid.set(thd->query_id);
@ -626,18 +916,19 @@ int ha_prepare(THD *thd)
{
int error=0, all=1;
THD_TRANS *trans=all ? &thd->transaction.all : &thd->transaction.stmt;
handlerton **ht=trans->ht;
Ha_trx_info *ha_info= trans->ha_list;
DBUG_ENTER("ha_prepare");
#ifdef USING_TRANSACTIONS
if (trans->nht)
if (ha_info)
{
for (; *ht; ht++)
for (; ha_info; ha_info= ha_info->next())
{
int err;
handlerton *ht= ha_info->ht();
status_var_increment(thd->status_var.ha_prepare_count);
if ((*ht)->prepare)
if (ht->prepare)
{
if ((err= (*(*ht)->prepare)(*ht, thd, all)))
if ((err= ht->prepare(ht, thd, all)))
{
my_error(ER_ERROR_DURING_COMMIT, MYF(0), err);
ha_rollback_trans(thd, all);
@ -649,7 +940,7 @@ int ha_prepare(THD *thd)
{
push_warning_printf(thd, MYSQL_ERROR::WARN_LEVEL_WARN,
ER_ILLEGAL_HA, ER(ER_ILLEGAL_HA),
ha_resolve_storage_engine_name(*ht));
ha_resolve_storage_engine_name(ht));
}
}
}
@ -657,6 +948,62 @@ int ha_prepare(THD *thd)
DBUG_RETURN(error);
}
/**
Check if we can skip the two-phase commit.
A helper function to evaluate if two-phase commit is mandatory.
As a side effect, propagates the read-only/read-write flags
of the statement transaction to its enclosing normal transaction.
@retval TRUE we must run a two-phase commit. Returned
if we have at least two engines with read-write changes.
@retval FALSE Don't need two-phase commit. Even if we have two
transactional engines, we can run two independent
commits if changes in one of the engines are read-only.
*/
static
bool
ha_check_and_coalesce_trx_read_only(THD *thd, Ha_trx_info *ha_list,
bool all)
{
/* The number of storage engines that have actual changes. */
unsigned rw_ha_count= 0;
Ha_trx_info *ha_info;
for (ha_info= ha_list; ha_info; ha_info= ha_info->next())
{
if (ha_info->is_trx_read_write())
++rw_ha_count;
if (! all)
{
Ha_trx_info *ha_info_all= &thd->ha_data[ha_info->ht()->slot].ha_info[1];
DBUG_ASSERT(ha_info != ha_info_all);
/*
Merge read-only/read-write information about statement
transaction to its enclosing normal transaction. Do this
only if in a real transaction -- that is, if we know
that ha_info_all is registered in thd->transaction.all.
Since otherwise we only clutter the normal transaction flags.
*/
if (ha_info_all->is_started()) /* FALSE if autocommit. */
ha_info_all->coalesce_trx_with(ha_info);
}
else if (rw_ha_count > 1)
{
/*
It is a normal transaction, so we don't need to merge read/write
information up, and the need for two-phase commit has been
already established. Break the loop prematurely.
*/
break;
}
}
return rw_ha_count > 1;
}
/**
@retval
0 ok
@ -674,12 +1021,25 @@ int ha_prepare(THD *thd)
int ha_commit_trans(THD *thd, bool all)
{
int error= 0, cookie= 0;
/*
'all' means that this is either an explicit commit issued by
user, or an implicit commit issued by a DDL.
*/
THD_TRANS *trans= all ? &thd->transaction.all : &thd->transaction.stmt;
bool is_real_trans= all || thd->transaction.all.nht == 0;
handlerton **ht= trans->ht;
bool is_real_trans= all || thd->transaction.all.ha_list == 0;
Ha_trx_info *ha_info= trans->ha_list;
my_xid xid= thd->transaction.xid_state.xid.get_my_xid();
DBUG_ENTER("ha_commit_trans");
/*
We must not commit the normal transaction if a statement
transaction is pending. Otherwise statement transaction
flags will not get propagated to its normal transaction's
counterpart.
*/
DBUG_ASSERT(thd->transaction.stmt.ha_list == NULL ||
trans == &thd->transaction.stmt);
if (thd->in_sub_stmt)
{
/*
@ -701,8 +1061,10 @@ int ha_commit_trans(THD *thd, bool all)
DBUG_RETURN(2);
}
#ifdef USING_TRANSACTIONS
if (trans->nht)
if (ha_info)
{
bool must_2pc;
if (is_real_trans && wait_if_global_read_lock(thd, 0, 0))
{
ha_rollback_trans(thd, all);
@ -727,12 +1089,26 @@ int ha_commit_trans(THD *thd, bool all)
if (is_real_trans) /* not a statement commit */
thd->stmt_map.close_transient_cursors();
if (!trans->no_2pc && trans->nht > 1)
must_2pc= ha_check_and_coalesce_trx_read_only(thd, ha_info, all);
if (!trans->no_2pc && must_2pc)
{
for (; *ht && !error; ht++)
for (; ha_info && !error; ha_info= ha_info->next())
{
int err;
if ((err= (*(*ht)->prepare)(*ht, thd, all)))
handlerton *ht= ha_info->ht();
/*
Do not call two-phase commit if this particular
transaction is read-only. This allows for simpler
implementation in engines that are always read-only.
*/
if (! ha_info->is_trx_read_write())
continue;
/*
Sic: we know that prepare() is not NULL since otherwise
trans->no_2pc would have been set.
*/
if ((err= ht->prepare(ht, thd, all)))
{
my_error(ER_ERROR_DURING_COMMIT, MYF(0), err);
error= 1;
@ -770,24 +1146,26 @@ int ha_commit_one_phase(THD *thd, bool all)
{
int error=0;
THD_TRANS *trans=all ? &thd->transaction.all : &thd->transaction.stmt;
bool is_real_trans=all || thd->transaction.all.nht == 0;
handlerton **ht=trans->ht;
bool is_real_trans=all || thd->transaction.all.ha_list == 0;
Ha_trx_info *ha_info= trans->ha_list, *ha_info_next;
DBUG_ENTER("ha_commit_one_phase");
#ifdef USING_TRANSACTIONS
if (trans->nht)
if (ha_info)
{
for (ht=trans->ht; *ht; ht++)
for (; ha_info; ha_info= ha_info_next)
{
int err;
if ((err= (*(*ht)->commit)(*ht, thd, all)))
handlerton *ht= ha_info->ht();
if ((err= ht->commit(ht, thd, all)))
{
my_error(ER_ERROR_DURING_COMMIT, MYF(0), err);
error=1;
}
status_var_increment(thd->status_var.ha_commit_count);
*ht= 0;
ha_info_next= ha_info->next();
ha_info->reset(); /* keep it conveniently zero-filled */
}
trans->nht=0;
trans->ha_list= 0;
trans->no_2pc=0;
if (is_real_trans)
thd->transaction.xid_state.xid.null();
@ -810,8 +1188,17 @@ int ha_rollback_trans(THD *thd, bool all)
{
int error=0;
THD_TRANS *trans=all ? &thd->transaction.all : &thd->transaction.stmt;
bool is_real_trans=all || thd->transaction.all.nht == 0;
Ha_trx_info *ha_info= trans->ha_list, *ha_info_next;
bool is_real_trans=all || thd->transaction.all.ha_list == 0;
DBUG_ENTER("ha_rollback_trans");
/*
We must not rollback the normal transaction if a statement
transaction is pending.
*/
DBUG_ASSERT(thd->transaction.stmt.ha_list == NULL ||
trans == &thd->transaction.stmt);
if (thd->in_sub_stmt)
{
/*
@ -826,24 +1213,26 @@ int ha_rollback_trans(THD *thd, bool all)
DBUG_RETURN(1);
}
#ifdef USING_TRANSACTIONS
if (trans->nht)
if (ha_info)
{
/* Close all cursors that can not survive ROLLBACK */
if (is_real_trans) /* not a statement commit */
thd->stmt_map.close_transient_cursors();
for (handlerton **ht=trans->ht; *ht; ht++)
for (; ha_info; ha_info= ha_info_next)
{
int err;
if ((err= (*(*ht)->rollback)(*ht, thd, all)))
handlerton *ht= ha_info->ht();
if ((err= ht->rollback(ht, thd, all)))
{ // cannot happen
my_error(ER_ERROR_DURING_ROLLBACK, MYF(0), err);
error=1;
}
status_var_increment(thd->status_var.ha_rollback_count);
*ht= 0;
ha_info_next= ha_info->next();
ha_info->reset(); /* keep it conveniently zero-filled */
}
trans->nht=0;
trans->ha_list= 0;
trans->no_2pc=0;
if (is_real_trans)
thd->transaction.xid_state.xid.null();
@ -889,17 +1278,19 @@ int ha_autocommit_or_rollback(THD *thd, int error)
{
DBUG_ENTER("ha_autocommit_or_rollback");
#ifdef USING_TRANSACTIONS
if (thd->transaction.stmt.nht)
if (thd->transaction.stmt.ha_list)
{
if (!error)
{
if (ha_commit_stmt(thd))
if (ha_commit_trans(thd, 0))
error=1;
}
else if (thd->transaction_rollback_request && !thd->in_sub_stmt)
(void) ha_rollback(thd);
else
(void) ha_rollback_stmt(thd);
else
{
(void) ha_rollback_trans(thd, 0);
if (thd->transaction_rollback_request && !thd->in_sub_stmt)
(void) ha_rollback(thd);
}
thd->variables.tx_isolation=thd->session_tx_isolation;
}
@ -1246,43 +1637,49 @@ int ha_rollback_to_savepoint(THD *thd, SAVEPOINT *sv)
int error=0;
THD_TRANS *trans= (thd->in_sub_stmt ? &thd->transaction.stmt :
&thd->transaction.all);
handlerton **ht=trans->ht, **end_ht;
Ha_trx_info *ha_info, *ha_info_next;
DBUG_ENTER("ha_rollback_to_savepoint");
trans->nht=sv->nht;
trans->no_2pc=0;
end_ht=ht+sv->nht;
/*
rolling back to savepoint in all storage engines that were part of the
transaction when the savepoint was set
*/
for (; ht < end_ht; ht++)
for (ha_info= sv->ha_list; ha_info; ha_info= ha_info->next())
{
int err;
DBUG_ASSERT((*ht)->savepoint_set != 0);
if ((err= (*(*ht)->savepoint_rollback)(*ht, thd, (uchar *)(sv+1)+(*ht)->savepoint_offset)))
handlerton *ht= ha_info->ht();
DBUG_ASSERT(ht);
DBUG_ASSERT(ht->savepoint_set != 0);
if ((err= ht->savepoint_rollback(ht, thd,
(uchar *)(sv+1)+ht->savepoint_offset)))
{ // cannot happen
my_error(ER_ERROR_DURING_ROLLBACK, MYF(0), err);
error=1;
}
status_var_increment(thd->status_var.ha_savepoint_rollback_count);
trans->no_2pc|=(*ht)->prepare == 0;
trans->no_2pc|= ht->prepare == 0;
}
/*
rolling back the transaction in all storage engines that were not part of
the transaction when the savepoint was set
*/
for (; *ht ; ht++)
for (ha_info= trans->ha_list; ha_info != sv->ha_list;
ha_info= ha_info_next)
{
int err;
if ((err= (*(*ht)->rollback)(*ht, thd, !thd->in_sub_stmt)))
handlerton *ht= ha_info->ht();
if ((err= ht->rollback(ht, thd, !thd->in_sub_stmt)))
{ // cannot happen
my_error(ER_ERROR_DURING_ROLLBACK, MYF(0), err);
error=1;
}
status_var_increment(thd->status_var.ha_rollback_count);
*ht=0; // keep it conveniently zero-filled
ha_info_next= ha_info->next();
ha_info->reset(); /* keep it conveniently zero-filled */
}
trans->ha_list= sv->ha_list;
DBUG_RETURN(error);
}
@ -1297,26 +1694,32 @@ int ha_savepoint(THD *thd, SAVEPOINT *sv)
int error=0;
THD_TRANS *trans= (thd->in_sub_stmt ? &thd->transaction.stmt :
&thd->transaction.all);
handlerton **ht=trans->ht;
Ha_trx_info *ha_info= trans->ha_list;
DBUG_ENTER("ha_savepoint");
#ifdef USING_TRANSACTIONS
for (; *ht; ht++)
for (; ha_info; ha_info= ha_info->next())
{
int err;
if (! (*ht)->savepoint_set)
handlerton *ht= ha_info->ht();
DBUG_ASSERT(ht);
if (! ht->savepoint_set)
{
my_error(ER_CHECK_NOT_IMPLEMENTED, MYF(0), "SAVEPOINT");
error=1;
break;
}
if ((err= (*(*ht)->savepoint_set)(*ht, thd, (uchar *)(sv+1)+(*ht)->savepoint_offset)))
if ((err= ht->savepoint_set(ht, thd, (uchar *)(sv+1)+ht->savepoint_offset)))
{ // cannot happen
my_error(ER_GET_ERRNO, MYF(0), err);
error=1;
}
status_var_increment(thd->status_var.ha_savepoint_count);
}
sv->nht=trans->nht;
/*
Remember the list of registered storage engines. All new
engines are prepended to the beginning of the list.
*/
sv->ha_list= trans->ha_list;
#endif /* USING_TRANSACTIONS */
DBUG_RETURN(error);
}
@ -1324,20 +1727,19 @@ int ha_savepoint(THD *thd, SAVEPOINT *sv)
int ha_release_savepoint(THD *thd, SAVEPOINT *sv)
{
int error=0;
THD_TRANS *trans= (thd->in_sub_stmt ? &thd->transaction.stmt :
&thd->transaction.all);
handlerton **ht=trans->ht, **end_ht;
Ha_trx_info *ha_info= sv->ha_list;
DBUG_ENTER("ha_release_savepoint");
end_ht=ht+sv->nht;
for (; ht < end_ht; ht++)
for (; ha_info; ha_info= ha_info->next())
{
int err;
if (!(*ht)->savepoint_release)
handlerton *ht= ha_info->ht();
/* Savepoint life time is enclosed into transaction life time. */
DBUG_ASSERT(ht);
if (!ht->savepoint_release)
continue;
if ((err= (*(*ht)->savepoint_release)(*ht, thd,
(uchar *)(sv+1)+
(*ht)->savepoint_offset)))
if ((err= ht->savepoint_release(ht, thd,
(uchar *)(sv+1) + ht->savepoint_offset)))
{ // cannot happen
my_error(ER_GET_ERRNO, MYF(0), err);
error=1;
@ -2506,6 +2908,36 @@ int handler::ha_check(THD *thd, HA_CHECK_OPT *check_opt)
return update_frm_version(table);
}
/**
A helper function to mark a transaction read-write,
if it is started.
*/
inline
void
handler::mark_trx_read_write()
{
Ha_trx_info *ha_info= &ha_thd()->ha_data[ht->slot].ha_info[0];
/*
When a storage engine method is called, the transaction must
have been started, unless it's a DDL call, for which the
storage engine starts the transaction internally, and commits
it internally, without registering in the ha_list.
Unfortunately here we can't know know for sure if the engine
has registered the transaction or not, so we must check.
*/
if (ha_info->is_started())
{
DBUG_ASSERT(has_transactions());
/*
table_share can be NULL in ha_delete_table(). See implementation
of standalone function ha_delete_table() in sql_base.cc.
*/
if (table_share == NULL || table_share->tmp_table == NO_TMP_TABLE)
ha_info->set_trx_read_write();
}
}
/**
Repair table: public interface.
@ -2516,6 +2948,9 @@ int handler::ha_check(THD *thd, HA_CHECK_OPT *check_opt)
int handler::ha_repair(THD* thd, HA_CHECK_OPT* check_opt)
{
int result;
mark_trx_read_write();
if ((result= repair(thd, check_opt)))
return result;
return update_frm_version(table);
@ -2532,6 +2967,8 @@ int
handler::ha_bulk_update_row(const uchar *old_data, uchar *new_data,
uint *dup_key_found)
{
mark_trx_read_write();
return bulk_update_row(old_data, new_data, dup_key_found);
}
@ -2545,6 +2982,8 @@ handler::ha_bulk_update_row(const uchar *old_data, uchar *new_data,
int
handler::ha_delete_all_rows()
{
mark_trx_read_write();
return delete_all_rows();
}
@ -2558,6 +2997,8 @@ handler::ha_delete_all_rows()
int
handler::ha_reset_auto_increment(ulonglong value)
{
mark_trx_read_write();
return reset_auto_increment(value);
}
@ -2571,6 +3012,8 @@ handler::ha_reset_auto_increment(ulonglong value)
int
handler::ha_backup(THD* thd, HA_CHECK_OPT* check_opt)
{
mark_trx_read_write();
return backup(thd, check_opt);
}
@ -2584,6 +3027,8 @@ handler::ha_backup(THD* thd, HA_CHECK_OPT* check_opt)
int
handler::ha_restore(THD* thd, HA_CHECK_OPT* check_opt)
{
mark_trx_read_write();
return restore(thd, check_opt);
}
@ -2597,6 +3042,8 @@ handler::ha_restore(THD* thd, HA_CHECK_OPT* check_opt)
int
handler::ha_optimize(THD* thd, HA_CHECK_OPT* check_opt)
{
mark_trx_read_write();
return optimize(thd, check_opt);
}
@ -2610,6 +3057,8 @@ handler::ha_optimize(THD* thd, HA_CHECK_OPT* check_opt)
int
handler::ha_analyze(THD* thd, HA_CHECK_OPT* check_opt)
{
mark_trx_read_write();
return analyze(thd, check_opt);
}
@ -2623,6 +3072,8 @@ handler::ha_analyze(THD* thd, HA_CHECK_OPT* check_opt)
bool
handler::ha_check_and_repair(THD *thd)
{
mark_trx_read_write();
return check_and_repair(thd);
}
@ -2636,6 +3087,8 @@ handler::ha_check_and_repair(THD *thd)
int
handler::ha_disable_indexes(uint mode)
{
mark_trx_read_write();
return disable_indexes(mode);
}
@ -2649,6 +3102,8 @@ handler::ha_disable_indexes(uint mode)
int
handler::ha_enable_indexes(uint mode)
{
mark_trx_read_write();
return enable_indexes(mode);
}
@ -2662,6 +3117,8 @@ handler::ha_enable_indexes(uint mode)
int
handler::ha_discard_or_import_tablespace(my_bool discard)
{
mark_trx_read_write();
return discard_or_import_tablespace(discard);
}
@ -2677,6 +3134,8 @@ handler::ha_discard_or_import_tablespace(my_bool discard)
void
handler::ha_prepare_for_alter()
{
mark_trx_read_write();
prepare_for_alter();
}
@ -2690,6 +3149,8 @@ handler::ha_prepare_for_alter()
int
handler::ha_rename_table(const char *from, const char *to)
{
mark_trx_read_write();
return rename_table(from, to);
}
@ -2703,6 +3164,8 @@ handler::ha_rename_table(const char *from, const char *to)
int
handler::ha_delete_table(const char *name)
{
mark_trx_read_write();
return delete_table(name);
}
@ -2716,6 +3179,8 @@ handler::ha_delete_table(const char *name)
void
handler::ha_drop_table(const char *name)
{
mark_trx_read_write();
return drop_table(name);
}
@ -2729,6 +3194,8 @@ handler::ha_drop_table(const char *name)
int
handler::ha_create(const char *name, TABLE *form, HA_CREATE_INFO *info)
{
mark_trx_read_write();
return create(name, form, info);
}
@ -2743,6 +3210,8 @@ int
handler::ha_create_handler_files(const char *name, const char *old_name,
int action_flag, HA_CREATE_INFO *info)
{
mark_trx_read_write();
return create_handler_files(name, old_name, action_flag, info);
}
@ -2761,6 +3230,8 @@ handler::ha_change_partitions(HA_CREATE_INFO *create_info,
const uchar *pack_frm_data,
size_t pack_frm_len)
{
mark_trx_read_write();
return change_partitions(create_info, path, copied, deleted,
pack_frm_data, pack_frm_len);
}
@ -2775,6 +3246,8 @@ handler::ha_change_partitions(HA_CREATE_INFO *create_info,
int
handler::ha_drop_partitions(const char *path)
{
mark_trx_read_write();
return drop_partitions(path);
}
@ -2788,6 +3261,8 @@ handler::ha_drop_partitions(const char *path)
int
handler::ha_rename_partitions(const char *path)
{
mark_trx_read_write();
return rename_partitions(path);
}
@ -2801,6 +3276,8 @@ handler::ha_rename_partitions(const char *path)
int
handler::ha_optimize_partitions(THD *thd)
{
mark_trx_read_write();
return optimize_partitions(thd);
}
@ -2814,6 +3291,8 @@ handler::ha_optimize_partitions(THD *thd)
int
handler::ha_analyze_partitions(THD *thd)
{
mark_trx_read_write();
return analyze_partitions(thd);
}
@ -2827,6 +3306,8 @@ handler::ha_analyze_partitions(THD *thd)
int
handler::ha_check_partitions(THD *thd)
{
mark_trx_read_write();
return check_partitions(thd);
}
@ -2840,6 +3321,8 @@ handler::ha_check_partitions(THD *thd)
int
handler::ha_repair_partitions(THD *thd)
{
mark_trx_read_write();
return repair_partitions(thd);
}
@ -2866,7 +3349,7 @@ int ha_enable_transaction(THD *thd, bool on)
is an optimization hint that storage engine is free to ignore.
So, let's commit an open transaction (if any) now.
*/
if (!(error= ha_commit_stmt(thd)))
if (!(error= ha_commit_trans(thd, 0)))
error= end_trans(thd, COMMIT);
}
DBUG_RETURN(error);
@ -4042,6 +4525,9 @@ int handler::ha_write_row(uchar *buf)
{
int error;
DBUG_ENTER("handler::ha_write_row");
mark_trx_read_write();
if (unlikely(error= write_row(buf)))
DBUG_RETURN(error);
if (unlikely(error= binlog_log_row<Write_rows_log_event>(table, 0, buf)))
@ -4060,6 +4546,8 @@ int handler::ha_update_row(const uchar *old_data, uchar *new_data)
*/
DBUG_ASSERT(new_data == table->record[0]);
mark_trx_read_write();
if (unlikely(error= update_row(old_data, new_data)))
return error;
if (unlikely(error= binlog_log_row<Update_rows_log_event>(table, old_data, new_data)))
@ -4070,6 +4558,9 @@ int handler::ha_update_row(const uchar *old_data, uchar *new_data)
int handler::ha_delete_row(const uchar *buf)
{
int error;
mark_trx_read_write();
if (unlikely(error= delete_row(buf)))
return error;
if (unlikely(error= binlog_log_row<Delete_rows_log_event>(table, buf, 0)))

View File

@ -721,14 +721,14 @@ struct handlerton
#define HTON_SUPPORT_LOG_TABLES (1 << 7) //Engine supports log tables
#define HTON_NO_PARTITION (1 << 8) //You can not partition these tables
typedef struct st_thd_trans
class Ha_trx_info;
struct THD_TRANS
{
/* number of entries in the ht[] */
uint nht;
/* true is not all entries in the ht[] support 2pc */
bool no_2pc;
/* storage engines that registered themselves for this transaction */
handlerton *ht[MAX_HA];
/* storage engines that registered in this transaction */
Ha_trx_info *ha_list;
/*
The purpose of this flag is to keep track of non-transactional
tables that were modified in scope of:
@ -758,7 +758,106 @@ typedef struct st_thd_trans
saved value.
*/
bool modified_non_trans_table;
} THD_TRANS;
void reset() { no_2pc= FALSE; modified_non_trans_table= FALSE; }
};
/**
Either statement transaction or normal transaction - related
thread-specific storage engine data.
If a storage engine participates in a statement/transaction,
an instance of this class is present in
thd->transaction.{stmt|all}.ha_list. The addition to
{stmt|all}.ha_list is made by trans_register_ha().
When it's time to commit or rollback, each element of ha_list
is used to access storage engine's prepare()/commit()/rollback()
methods, and also to evaluate if a full two phase commit is
necessary.
@sa General description of transaction handling in handler.cc.
*/
class Ha_trx_info
{
public:
/** Register this storage engine in the given transaction context. */
void register_ha(THD_TRANS *trans, handlerton *ht_arg)
{
DBUG_ASSERT(m_flags == 0);
DBUG_ASSERT(m_ht == NULL);
DBUG_ASSERT(m_next == NULL);
m_ht= ht_arg;
m_flags= (int) TRX_READ_ONLY; /* Assume read-only at start. */
m_next= trans->ha_list;
trans->ha_list= this;
}
/** Clear, prepare for reuse. */
void reset()
{
m_next= NULL;
m_ht= NULL;
m_flags= 0;
}
Ha_trx_info() { reset(); }
void set_trx_read_write()
{
DBUG_ASSERT(is_started());
m_flags|= (int) TRX_READ_WRITE;
}
bool is_trx_read_write() const
{
DBUG_ASSERT(is_started());
return m_flags & (int) TRX_READ_WRITE;
}
bool is_started() const { return m_ht != NULL; }
/** Mark this transaction read-write if the argument is read-write. */
void coalesce_trx_with(const Ha_trx_info *stmt_trx)
{
/*
Must be called only after the transaction has been started.
Can be called many times, e.g. when we have many
read-write statements in a transaction.
*/
DBUG_ASSERT(is_started());
if (stmt_trx->is_trx_read_write())
set_trx_read_write();
}
Ha_trx_info *next() const
{
DBUG_ASSERT(is_started());
return m_next;
}
handlerton *ht() const
{
DBUG_ASSERT(is_started());
return m_ht;
}
private:
enum { TRX_READ_ONLY= 0, TRX_READ_WRITE= 1 };
/** Auxiliary, used for ha_list management */
Ha_trx_info *m_next;
/**
Although a given Ha_trx_info instance is currently always used
for the same storage engine, 'ht' is not-NULL only when the
corresponding storage is a part of a transaction.
*/
handlerton *m_ht;
/**
Transaction flags related to this engine.
Not-null only if this instance is a part of transaction.
May assume a combination of enum values above.
*/
uchar m_flags;
};
enum enum_tx_isolation { ISO_READ_UNCOMMITTED, ISO_READ_COMMITTED,
ISO_REPEATABLE_READ, ISO_SERIALIZABLE};
@ -1640,7 +1739,14 @@ protected:
provide useful functionality.
*/
virtual int rename_table(const char *from, const char *to);
/**
Delete a table in the engine. Called for base as well as temporary
tables.
*/
virtual int delete_table(const char *name);
private:
/* Private helpers */
inline void mark_trx_read_write();
private:
/*
Low-level primitives for storage engines. These should be
@ -1821,9 +1927,7 @@ extern TYPELIB tx_isolation_typelib;
extern TYPELIB myisam_stats_method_typelib;
extern ulong total_ha, total_ha_2pc;
/* Wrapper functions */
#define ha_commit_stmt(thd) (ha_commit_trans((thd), FALSE))
#define ha_rollback_stmt(thd) (ha_rollback_trans((thd), FALSE))
/* Wrapper functions */
#define ha_commit(thd) (ha_commit_trans((thd), TRUE))
#define ha_rollback(thd) (ha_rollback_trans((thd), TRUE))

View File

@ -3332,6 +3332,16 @@ THD::binlog_start_trans_and_stmt()
if (options & (OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN))
trans_register_ha(this, TRUE, binlog_hton);
trans_register_ha(this, FALSE, binlog_hton);
/*
Mark statement transaction as read/write. We never start
a binary log transaction and keep it read-only,
therefore it's best to mark the transaction read/write just
at the same time we start it.
Not necessary to mark the normal transaction read/write
since the statement-level flag will be propagated automatically
inside ha_commit_trans.
*/
ha_data[binlog_hton->slot].ha_info[0].set_trx_read_write();
}
DBUG_VOID_RETURN;
}

View File

@ -2898,7 +2898,7 @@ int Format_description_log_event::do_apply_event(Relay_log_info const *rli)
original place when it comes to us; we'll know this by checking
log_pos ("artificial" events have log_pos == 0).
*/
if (!artificial_event && created && thd->transaction.all.nht)
if (!artificial_event && created && thd->transaction.all.ha_list)
{
/* This is not an error (XA is safe), just an information */
rli->report(INFORMATION_LEVEL, 0,

View File

@ -62,6 +62,26 @@ int injector::transaction::commit()
{
DBUG_ENTER("injector::transaction::commit()");
m_thd->binlog_flush_pending_rows_event(true);
/*
Cluster replication does not preserve statement or
transaction boundaries of the master. Instead, a new
transaction on replication slave is started when a new GCI
(global checkpoint identifier) is issued, and is committed
when the last event of the check point has been received and
processed. This ensures consistency of each cluster in
cluster replication, and there is no requirement for stronger
consistency: MySQL replication is asynchronous with other
engines as well.
A practical consequence of that is that row level replication
stream passed through the injector thread never contains
COMMIT events.
Here we should preserve the server invariant that there is no
outstanding statement transaction when the normal transaction
is committed by committing the statement transaction
explicitly.
*/
ha_autocommit_or_rollback(m_thd, 0);
end_trans(m_thd, COMMIT);
DBUG_RETURN(0);
}

View File

@ -665,8 +665,16 @@ sp_returns_type(THD *thd, String &result, sp_head *sp)
(TYPE_ENUM_PROCEDURE or TYPE_ENUM_FUNCTION).
@param sp Stored routine object to store.
@return Error code. SP_OK is returned on success. Other SP_ constants are
used to indicate about errors.
@note Opens and closes the thread tables. Therefore assumes
that there are no locked tables in this thread at the time of
invocation.
Unlike some other DDL statements, *does* close the tables
in the end, since the call to this function is normally
followed by an implicit grant (sp_grant_privileges())
and this subsequent call opens and closes mysql.procs_priv.
@return Error code. SP_OK is returned on success. Other
SP_ constants are used to indicate about errors.
*/
int
@ -1223,7 +1231,13 @@ done:
}
/* Drop all routines in database 'db' */
/**
Drop all routines in database 'db'
@note Close the thread tables, the calling code might want to
delete from other system tables afterwards.
*/
int
sp_drop_db_routines(THD *thd, char *db)
{

View File

@ -2700,6 +2700,7 @@ sp_lex_keeper::reset_lex_and_exec_core(THD *thd, uint *nextp,
m_lex->unit.cleanup();
thd_proc_info(thd, "closing tables");
/* Here we also commit or rollback the current statement. */
close_thread_tables(thd);
thd_proc_info(thd, 0);

View File

@ -1324,29 +1324,45 @@ void close_thread_tables(THD *thd)
Mark all temporary tables used by this statement as free for reuse.
*/
mark_temp_tables_as_free_for_reuse(thd);
/*
Let us commit transaction for statement. Since in 5.0 we only have
one statement transaction and don't allow several nested statement
transactions this call will do nothing if we are inside of stored
function or trigger (i.e. statement transaction is already active and
does not belong to statement for which we do close_thread_tables()).
TODO: This should be fixed in later releases.
*/
if (!(thd->state_flags & Open_tables_state::BACKUPS_AVAIL))
{
thd->main_da.can_overwrite_status= TRUE;
ha_autocommit_or_rollback(thd, thd->is_error());
thd->main_da.can_overwrite_status= FALSE;
/*
Reset transaction state, but only if we're not inside a
sub-statement of a prelocked statement.
*/
if (! prelocked_mode || thd->lex->requires_prelocking())
thd->transaction.stmt.reset();
}
if (thd->locked_tables || prelocked_mode)
{
/*
Let us commit transaction for statement. Since in 5.0 we only have
one statement transaction and don't allow several nested statement
transactions this call will do nothing if we are inside of stored
function or trigger (i.e. statement transaction is already active and
does not belong to statement for which we do close_thread_tables()).
TODO: This should be fixed in later releases.
*/
ha_commit_stmt(thd);
/* Ensure we are calling ha_reset() for all used tables */
mark_used_tables_as_free_for_reuse(thd, thd->open_tables);
/* We are under simple LOCK TABLES so should not do anything else. */
/*
We are under simple LOCK TABLES or we're inside a sub-statement
of a prelocked statement, so should not do anything else.
*/
if (!prelocked_mode || !thd->lex->requires_prelocking())
DBUG_VOID_RETURN;
/*
We are in prelocked mode, so we have to leave it now with doing
implicit UNLOCK TABLES if need.
We are in the top-level statement of a prelocked statement,
so we have to leave the prelocked mode now with doing implicit
UNLOCK TABLES if needed.
*/
DBUG_PRINT("info",("thd->prelocked_mode= NON_PRELOCKED"));
thd->prelocked_mode= NON_PRELOCKED;
@ -1374,19 +1390,6 @@ void close_thread_tables(THD *thd)
mysql_unlock_tables(thd, thd->lock);
thd->lock=0;
}
/*
assume handlers auto-commit (if some doesn't - transaction handling
in MySQL should be redesigned to support it; it's a big change,
and it's not worth it - better to commit explicitly only writing
transactions, read-only ones should better take care of themselves.
saves some work in 2pc too)
see also sql_parse.cc - dispatch_command()
*/
if (!(thd->state_flags & Open_tables_state::BACKUPS_AVAIL))
bzero(&thd->transaction.stmt, sizeof(thd->transaction.stmt));
if (!thd->active_transaction())
thd->transaction.xid_state.xid.null();
/*
Note that we need to hold LOCK_open while changing the
open_tables list. Another thread may work on it.
@ -5059,10 +5062,7 @@ int decide_logging_format(THD *thd, TABLE_LIST *tables)
DBUG_PRINT("info", ("error: %d", error));
if (error)
{
ha_rollback_stmt(thd);
return -1;
}
/*
We switch to row-based format if we are in mixed mode and one of
@ -5216,7 +5216,6 @@ int lock_tables(THD *thd, TABLE_LIST *tables, uint count, bool *need_reopen)
table->table->query_id= thd->query_id;
if (check_lock_and_start_stmt(thd, table->table, table->lock_type))
{
ha_rollback_stmt(thd);
mysql_unlock_tables(thd, thd->locked_tables);
thd->locked_tables= 0;
thd->options&= ~(OPTION_TABLE_LOCK);
@ -5251,7 +5250,6 @@ int lock_tables(THD *thd, TABLE_LIST *tables, uint count, bool *need_reopen)
if (!table->placeholder() &&
check_lock_and_start_stmt(thd, table->table, table->lock_type))
{
ha_rollback_stmt(thd);
DBUG_RETURN(-1);
}
}

View File

@ -264,7 +264,7 @@ const char *set_thd_proc_info(THD *thd, const char *info,
extern "C"
void **thd_ha_data(const THD *thd, const struct handlerton *hton)
{
return (void **) thd->ha_data + hton->slot;
return (void **) &thd->ha_data[hton->slot].ha_ptr;
}
extern "C"
@ -2513,7 +2513,7 @@ bool select_dumpvar::send_data(List<Item> &items)
suv->update();
}
}
DBUG_RETURN(0);
DBUG_RETURN(thd->is_error());
}
bool select_dumpvar::send_eof()

View File

@ -686,7 +686,8 @@ private:
struct st_savepoint {
struct st_savepoint *prev;
char *name;
uint length, nht;
uint length;
Ha_trx_info *ha_list;
};
enum xa_states {XA_NOTR=0, XA_ACTIVE, XA_IDLE, XA_PREPARED};
@ -1092,6 +1093,33 @@ private:
};
/**
Storage engine specific thread local data.
*/
struct Ha_data
{
/**
Storage engine specific thread local data.
Lifetime: one user connection.
*/
void *ha_ptr;
/**
0: Life time: one statement within a transaction. If @@autocommit is
on, also represents the entire transaction.
@sa trans_register_ha()
1: Life time: one transaction within a connection.
If the storage engine does not participate in a transaction,
this should not be used.
@sa trans_register_ha()
*/
Ha_trx_info ha_info[2];
Ha_data() :ha_ptr(NULL) {}
};
/**
@class THD
For each client connection we create a separate thread with THD serving as
@ -1231,7 +1259,7 @@ public:
uint in_sub_stmt;
/* container for handler's private per-connection data */
void *ha_data[MAX_HA];
Ha_data ha_data[MAX_HA];
#ifndef MYSQL_CLIENT
int binlog_setup_trx_data();

View File

@ -322,9 +322,10 @@ Sensitive_cursor::post_open(THD *thd)
close_at_commit= FALSE; /* reset in case we're reusing the cursor */
info= &ht_info[0];
for (handlerton **pht= thd->transaction.stmt.ht; *pht; pht++)
for (Ha_trx_info *ha_trx_info= thd->transaction.stmt.ha_list;
ha_trx_info; ha_trx_info= ha_trx_info->next())
{
handlerton *ht= *pht;
handlerton *ht= ha_trx_info->ht();
close_at_commit|= test(ht->flags & HTON_CLOSE_CURSORS_AT_COMMIT);
if (ht->create_cursor_read_view)
{

View File

@ -24,6 +24,14 @@
#include "sp_head.h"
#include "sql_trigger.h"
/**
Implement DELETE SQL word.
@note Like implementations of other DDL/DML in MySQL, this function
relies on the caller to close the thread tables. This is done in the
end of dispatch_command().
*/
bool mysql_delete(THD *thd, TABLE_LIST *table_list, COND *conds,
SQL_LIST *order, ha_rows limit, ulonglong options,
bool reset_auto_increment)
@ -380,17 +388,6 @@ cleanup:
}
DBUG_ASSERT(transactional_table || !deleted || thd->transaction.stmt.modified_non_trans_table);
free_underlaid_joins(thd, select_lex);
if (transactional_table)
{
if (ha_autocommit_or_rollback(thd,error >= 0))
error=1;
}
if (thd->lock)
{
mysql_unlock_tables(thd, thd->lock);
thd->lock=0;
}
if (error < 0 || (thd->lex->ignore && !thd->is_fatal_error))
{
thd->row_count_func= deleted;
@ -751,11 +748,9 @@ void multi_delete::abort()
The same if all tables are transactional, regardless of where we are.
In all other cases do attempt deletes ...
*/
if ((table_being_deleted == delete_tables &&
table_being_deleted->table->file->has_transactions()) ||
!normal_tables)
ha_rollback_stmt(thd);
else if (do_delete)
if (do_delete && normal_tables &&
(table_being_deleted != delete_tables ||
!table_being_deleted->table->file->has_transactions()))
{
/*
We have to execute the recorded do_deletes() and write info into the
@ -921,11 +916,6 @@ bool multi_delete::send_eof()
if (local_error != 0)
error_handled= TRUE; // to force early leave from ::send_error()
/* Commit or rollback the current SQL statement */
if (transactional_tables)
if (ha_autocommit_or_rollback(thd,local_error > 0))
local_error=1;
if (!local_error)
{
thd->row_count_func= deleted;
@ -1055,6 +1045,12 @@ trunc_by_del:
error= mysql_delete(thd, table_list, (COND*) 0, (SQL_LIST*) 0,
HA_POS_ERROR, LL(0), TRUE);
ha_enable_transaction(thd, TRUE);
/*
Safety, in case the engine ignored ha_enable_transaction(FALSE)
above. Also clears thd->transaction.*.
*/
error= ha_autocommit_or_rollback(thd, error);
ha_commit(thd);
thd->options= save_options;
thd->current_stmt_binlog_row_based= save_binlog_row_based;
DBUG_RETURN(error);

View File

@ -28,7 +28,17 @@ bool mysql_do(THD *thd, List<Item> &values)
while ((value = li++))
value->val_int();
free_underlaid_joins(thd, &thd->lex->select_lex);
thd->clear_error(); // DO always is OK
if (thd->is_error())
{
/*
Rollback the effect of the statement, since next instruction
will clear the error and the rollback in the end of
dispatch_command() won't work.
*/
ha_autocommit_or_rollback(thd, thd->is_error());
thd->clear_error(); // DO always is OK
}
send_ok(thd);
DBUG_RETURN(FALSE);
}

View File

@ -541,6 +541,10 @@ bool open_and_lock_for_insert_delayed(THD *thd, TABLE_LIST *table_list)
/**
INSERT statement implementation
@note Like implementations of other DDL/DML in MySQL, this function
relies on the caller to close the thread tables. This is done in the
end of dispatch_command().
*/
bool mysql_insert(THD *thd,TABLE_LIST *table_list,
@ -893,12 +897,9 @@ bool mysql_insert(THD *thd,TABLE_LIST *table_list,
}
DBUG_ASSERT(transactional_table || !changed ||
thd->transaction.stmt.modified_non_trans_table);
if (transactional_table)
error=ha_autocommit_or_rollback(thd,error);
if (thd->lock)
{
mysql_unlock_tables(thd, thd->lock);
/*
Invalidate the table in the query cache if something changed
after unlocking when changes become fisible.
@ -909,7 +910,6 @@ bool mysql_insert(THD *thd,TABLE_LIST *table_list,
{
query_cache_invalidate3(thd, table_list, 1);
}
thd->lock=0;
}
}
thd_proc_info(thd, "end");
@ -2445,7 +2445,7 @@ err:
first call to ha_*_row() instead. Remove code that are used to
cover for the case outlined above.
*/
ha_rollback_stmt(thd);
ha_autocommit_or_rollback(thd, 1);
#ifndef __WIN__
end:
@ -3139,18 +3139,6 @@ bool select_insert::send_eof()
thd->query, thd->query_length,
trans_table, FALSE, killed_status);
}
/*
We will call ha_autocommit_or_rollback() also for
non-transactional tables under row-based replication: there might
be events in the binary logs transaction, and we need to write
them to the binary log.
*/
if (trans_table || thd->current_stmt_binlog_row_based)
{
int error2= ha_autocommit_or_rollback(thd, error);
if (error2 && !error)
error= error2;
}
table->file->ha_release_auto_increment();
if (error)
@ -3228,7 +3216,6 @@ void select_insert::abort() {
table->file->ha_release_auto_increment();
}
ha_rollback_stmt(thd);
DBUG_VOID_RETURN;
}
@ -3667,7 +3654,10 @@ bool select_create::send_eof()
nevertheless.
*/
if (!table->s->tmp_table)
ha_commit(thd); // Can fail, but we proceed anyway
{
ha_autocommit_or_rollback(thd, 0);
end_active_trans(thd);
}
table->file->extra(HA_EXTRA_NO_IGNORE_DUP_KEY);
table->file->extra(HA_EXTRA_WRITE_CANNOT_REPLACE);
@ -3691,12 +3681,9 @@ void select_create::abort()
by removing the table, even for non-transactional tables.
*/
tmp_disable_binlog(thd);
select_insert::abort();
reenable_binlog(thd);
/*
We roll back the statement, including truncating the transaction
cache of the binary log, if the statement failed.
In select_insert::abort() we roll back the statement, including
truncating the transaction cache of the binary log.
We roll back the statement prior to deleting the table and prior
to releasing the lock on the table, since there might be potential
@ -3707,8 +3694,9 @@ void select_create::abort()
of the table succeeded or not, since we need to reset the binary
log state.
*/
if (thd->current_stmt_binlog_row_based)
ha_rollback_stmt(thd);
select_insert::abort();
reenable_binlog(thd);
if (m_plock)
{

View File

@ -470,9 +470,6 @@ bool mysql_load(THD *thd,sql_exchange *ex,TABLE_LIST *table_list,
}
}
#endif /*!EMBEDDED_LIBRARY*/
if (transactional_table)
ha_autocommit_or_rollback(thd,error);
error= -1; // Error on read
goto err;
}
@ -510,8 +507,6 @@ bool mysql_load(THD *thd,sql_exchange *ex,TABLE_LIST *table_list,
}
}
#endif /*!EMBEDDED_LIBRARY*/
if (transactional_table)
error=ha_autocommit_or_rollback(thd,error);
/* ok to client sent only after binlog write and engine commit */
send_ok(thd, info.copied + info.deleted, 0L, name);
@ -519,11 +514,6 @@ err:
DBUG_ASSERT(transactional_table || !(info.copied || info.deleted) ||
thd->transaction.stmt.modified_non_trans_table);
table->file->ha_release_auto_increment();
if (thd->lock)
{
mysql_unlock_tables(thd, thd->lock);
thd->lock=0;
}
table->auto_increment_field_not_null= FALSE;
thd->abort_on_warning= 0;
DBUG_RETURN(error);

View File

@ -1465,21 +1465,13 @@ bool dispatch_command(enum enum_server_command command, THD *thd,
break;
}
thd_proc_info(thd, "closing tables");
/* Free tables */
close_thread_tables(thd);
/* If commit fails, we should be able to reset the OK status. */
thd->main_da.can_overwrite_status= TRUE;
ha_autocommit_or_rollback(thd, thd->is_error());
thd->main_da.can_overwrite_status= FALSE;
thd->transaction.stmt.reset();
/*
assume handlers auto-commit (if some doesn't - transaction handling
in MySQL should be redesigned to support it; it's a big change,
and it's not worth it - better to commit explicitly only writing
transactions, read-only ones should better take care of themselves.
saves some work in 2pc too)
see also sql_base.cc - close_thread_tables()
*/
bzero(&thd->transaction.stmt, sizeof(thd->transaction.stmt));
if (!thd->active_transaction())
thd->transaction.xid_state.xid.null();
/* report error issued during command execution */
if (thd->killed_errno())
@ -1496,6 +1488,10 @@ bool dispatch_command(enum enum_server_command command, THD *thd,
net_end_statement(thd);
query_cache_end_of_result(thd);
thd->proc_info= "closing tables";
/* Free tables */
close_thread_tables(thd);
log_slow_statement(thd);
thd_proc_info(thd, "cleaning up");
@ -3011,10 +3007,8 @@ end_with_restore_list:
/* INSERT ... SELECT should invalidate only the very first table */
TABLE_LIST *save_table= first_table->next_local;
first_table->next_local= 0;
mysql_unlock_tables(thd, thd->lock);
query_cache_invalidate3(thd, first_table, 1);
first_table->next_local= save_table;
thd->lock=0;
}
delete sel_result;
}
@ -3985,7 +3979,6 @@ end_with_restore_list:
push_warning(thd, MYSQL_ERROR::WARN_LEVEL_WARN,
ER_PROC_AUTO_GRANT_FAIL,
ER(ER_PROC_AUTO_GRANT_FAIL));
close_thread_tables(thd);
}
#endif
break;

View File

@ -3968,31 +3968,35 @@ static int fast_end_partition(THD *thd, ulonglong copied,
bool written_bin_log)
{
int error;
char tmp_name[80];
DBUG_ENTER("fast_end_partition");
thd->proc_info="end";
if (!is_empty)
query_cache_invalidate3(thd, table_list, 0);
error= ha_commit_stmt(thd);
if (ha_commit(thd))
error= ha_autocommit_or_rollback(thd, 0);
if (end_active_trans(thd))
error= 1;
if (!error || is_empty)
if (error)
{
char tmp_name[80];
if ((!is_empty) && (!written_bin_log) &&
(!thd->lex->no_write_to_binlog))
write_bin_log(thd, FALSE, thd->query, thd->query_length);
close_thread_tables(thd);
my_snprintf(tmp_name, sizeof(tmp_name), ER(ER_INSERT_INFO),
(ulong) (copied + deleted),
(ulong) deleted,
(ulong) 0);
send_ok(thd, (ha_rows) (copied+deleted),0L,tmp_name);
DBUG_RETURN(FALSE);
/* If error during commit, no need to rollback, it's done. */
table->file->print_error(error, MYF(0));
DBUG_RETURN(TRUE);
}
table->file->print_error(error, MYF(0));
close_thread_tables(thd);
DBUG_RETURN(TRUE);
if ((!is_empty) && (!written_bin_log) &&
(!thd->lex->no_write_to_binlog))
write_bin_log(thd, FALSE, thd->query, thd->query_length);
my_snprintf(tmp_name, sizeof(tmp_name), ER(ER_INSERT_INFO),
(ulong) (copied + deleted),
(ulong) deleted,
(ulong) 0);
send_ok(thd, (ha_rows) (copied+deleted),0L, tmp_name);
DBUG_RETURN(FALSE);
}

View File

@ -4131,6 +4131,7 @@ static bool mysql_admin_table(THD* thd, TABLE_LIST* tables,
switch ((*prepare_func)(thd, table, check_opt)) {
case 1: // error, message written to net
ha_autocommit_or_rollback(thd, 1);
end_trans(thd, ROLLBACK);
close_thread_tables(thd);
DBUG_PRINT("admin", ("simple error, admin next table"));
continue;
@ -4189,6 +4190,7 @@ static bool mysql_admin_table(THD* thd, TABLE_LIST* tables,
table_name);
protocol->store(buff, length, system_charset_info);
ha_autocommit_or_rollback(thd, 0);
end_trans(thd, COMMIT);
close_thread_tables(thd);
lex->reset_query_tables_list(FALSE);
table->table=0; // For query cache
@ -4461,6 +4463,7 @@ send_result_message:
}
}
ha_autocommit_or_rollback(thd, 0);
end_trans(thd, COMMIT);
close_thread_tables(thd);
table->table=0; // For query cache
if (protocol->write())
@ -4470,8 +4473,9 @@ send_result_message:
send_eof(thd);
DBUG_RETURN(FALSE);
err:
err:
ha_autocommit_or_rollback(thd, 1);
end_trans(thd, ROLLBACK);
close_thread_tables(thd); // Shouldn't be needed
if (table)
table->table=0;
@ -4994,8 +4998,8 @@ mysql_discard_or_import_tablespace(THD *thd,
query_cache_invalidate3(thd, table_list, 0);
/* The ALTER TABLE is always in its own transaction */
error = ha_commit_stmt(thd);
if (ha_commit(thd))
error = ha_autocommit_or_rollback(thd, 0);
if (end_active_trans(thd))
error=1;
if (error)
goto err;
@ -5003,7 +5007,6 @@ mysql_discard_or_import_tablespace(THD *thd,
err:
ha_autocommit_or_rollback(thd, error);
close_thread_tables(thd);
thd->tablespace_op=FALSE;
if (error == 0)
@ -6526,8 +6529,8 @@ view_err:
VOID(pthread_mutex_unlock(&LOCK_open));
alter_table_manage_keys(table, table->file->indexes_are_disabled(),
alter_info->keys_onoff);
error= ha_commit_stmt(thd);
if (ha_commit(thd))
error= ha_autocommit_or_rollback(thd, 0);
if (end_active_trans(thd))
error= 1;
}
thd->count_cuted_fields= CHECK_FIELD_IGNORE;
@ -6615,7 +6618,7 @@ view_err:
/* Need to commit before a table is unlocked (NDB requirement). */
DBUG_PRINT("info", ("Committing before unlocking table"));
if (ha_commit_stmt(thd) || ha_commit(thd))
if (ha_autocommit_or_rollback(thd, 0) || end_active_trans(thd))
goto err1;
committed= 1;
}
@ -7116,9 +7119,9 @@ copy_data_between_tables(TABLE *from,TABLE *to,
Ensure that the new table is saved properly to disk so that we
can do a rename
*/
if (ha_commit_stmt(thd))
if (ha_autocommit_or_rollback(thd, 0))
error=1;
if (ha_commit(thd))
if (end_active_trans(thd))
error=1;
err:

View File

@ -382,6 +382,14 @@ static udf_func *add_udf(LEX_STRING *name, Item_result ret, char *dl,
}
/**
Create a user defined function.
@note Like implementations of other DDL/DML in MySQL, this function
relies on the caller to close the thread tables. This is done in the
end of dispatch_command().
*/
int mysql_create_function(THD *thd,udf_func *udf)
{
int error;
@ -489,7 +497,6 @@ int mysql_create_function(THD *thd,udf_func *udf)
table->field[3]->store((longlong) u_d->type, TRUE);
error = table->file->ha_write_row(table->record[0]);
close_thread_tables(thd);
if (error)
{
my_error(ER_ERROR_ON_WRITE, MYF(0), "mysql.func", error);

View File

@ -803,17 +803,6 @@ int mysql_update(THD *thd,
}
DBUG_ASSERT(transactional_table || !updated || thd->transaction.stmt.modified_non_trans_table);
free_underlaid_joins(thd, select_lex);
if (transactional_table)
{
if (ha_autocommit_or_rollback(thd, error >= 0))
error=1;
}
if (thd->lock)
{
mysql_unlock_tables(thd, thd->lock);
thd->lock=0;
}
/* If LAST_INSERT_ID(X) was used, report X */
id= thd->arg_of_last_insert_id_function ?
@ -1716,13 +1705,8 @@ void multi_update::abort()
If not attempt to do remaining updates.
*/
if (trans_safe)
if (! trans_safe)
{
DBUG_ASSERT(transactional_tables);
(void) ha_autocommit_or_rollback(thd, 1);
}
else
{
DBUG_ASSERT(thd->transaction.stmt.modified_non_trans_table);
if (do_update && table_count > 1)
{
@ -1754,11 +1738,6 @@ void multi_update::abort()
thd->transaction.all.modified_non_trans_table= TRUE;
}
DBUG_ASSERT(trans_safe || !updated || thd->transaction.stmt.modified_non_trans_table);
if (transactional_tables)
{
(void) ha_autocommit_or_rollback(thd, 1);
}
}
@ -1996,12 +1975,6 @@ bool multi_update::send_eof()
if (local_error != 0)
error_handled= TRUE; // to force early leave from ::send_error()
if (transactional_tables)
{
if (ha_autocommit_or_rollback(thd, local_error != 0))
local_error=1;
}
if (local_error > 0) // if the above log write did not fail ...
{
/* Safety: If we haven't got an error before (can happen in do_updates) */