My MySQL procedure looks like:
create procedure create_feed (_id int)
begin
declare exit handler for sqlexception
begin
rollback;
select false;
end;
start transaction;
insert into t1(id)
values (_id);
insert into wrong_table_name (id, createdtime)
values (
_id,
CURRENT_TIMESTAMP
);
commit;
select true;
end//
After I called this procedure, the t1 table is updated, and the value 'true' is returned. The wrong_table_name does not exist at all. Why?
I recommend that you use a function instead of a procedure if you want it to return a value. Another option is to use out parameters if you want your procedure to return one or more values.
Nonetheless, I am surprised by your results. If wrong_table_name does not exist, then that proc should return true. As for t1 being updated, that would happen if you are using a non-transactional storage engine like MyISAM, which ignores the rollback.
I tested your code in MySQL 5.5.8, and it worked properly for me. Namely, it always entered the exit handler when wrong_table_name did not exist.
Related
I have two tables. Mysql 5.7 , Tables have Innodb storage engine.
create table test_tran (id integer, descr varchar(255));
create table test_enum (id int, flag enum('good','not good') not null);
A valid record is insert into test_enum table
insert into test_enum values(1,'good')
I have stored procedure with transaction defined which tries to update an invalid value in the enum column. It also catches the error to rollback the transaction, then resignals the error.
SET sql_mode = 'STRICT_ALL_TABLES';
DROP PROCEDURE IF EXISTS test_tran;
DELIMITER $$
CREATE PROCEDURE test_tran
(
i_case integer
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
IF i_case=0 THEN
insert into test_tran values (1, 'abc');
update test_enum set flag='random';
ELSE
insert into test_tran values (3, 'else_1_abc');
insert into test_tran values (4, 'else_2_abc');
END IF;
COMMIT;
END $$
DELIMITER ;
I execute
call test_tran(0)
and get an error - "Error Code: 1265. Data truncated for column 'flag' at row"
I was expecting the below statement to rollback
insert into test_tran values (1, 'abc');
However, I see it getting committed and I see the row inserted in test_tran table. What is causing the issue?
The key piece of information is missing from the error message: the SQLSTATE. The full error message should be:
ERROR: 1265 (01000): Data truncated for column 'flag' at row 1
(Likely the client being used isn't including the SQLSTATE in the error message; the above is what MySQL Shell produces.)
The documentation for DECLARE ... HANDLER states:
The condition_value for DECLARE ... HANDLER indicates the specific condition or class of conditions that activates the handler. It can take the following forms:
...
SQLWARNING: Shorthand for the class of SQLSTATE values that begin with '01'.
...
SQLEXCEPTION: Shorthand for the class of SQLSTATE values that do not begin with '00', '01', or '02'.
The error generated by setting an invalid enum column is part of the SQLWARNING class, not SQLEXCEPTION, so the error handler is never invoked and the transaction is still active after the procedure exits, which is why the inserted row appears in test_tran. Note the transaction could be rolled back manually at that point; also, closing the session will cause an automatic rollback.
To handle the error in test_tran, simply change SQLEXCEPTION to SQLWARNING, or (perhaps more appropriate) add conditions to the handler:
CREATE PROCEDURE test_tran (i_case integer)
BEGIN
DECLARE EXIT HANDLER FOR SQLWARNING, NOT FOUND, SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
...
Note for other readers: some may be wondering why the DECLARE ... HANDLER is needed; why the entire transaction isn't cancelled by the error. The InnoDB error handling page describes when implicit rollbacks occur. The only time the entire transaction is automatically rolled back is if there's a transaction deadlock, or if a lock times out while waiting and the server was started with --innodb-rollback-on-timeout set. Setting a column to an invalid enum value
[is] mostly detected by the MySQL layer of code (above the InnoDB storage engine level), and they roll back the corresponding SQL statement. Locks are not released in a rollback of a single SQL statement.
In other words, the query that set the invalid enum value is rolled back, but the transaction is not.
I am very new in Mysql, probably don't know or don't understand something essential.
Could you please advise me why 'begin !!!' message is not inserted in this
case?
DELIMITER $$
CREATE TABLE `_debugLogTable` (
`Message` varchar(255) DEFAULT NULL
) ENGINE=InnoDB $$
CREATE PROCEDURE `debug_msg`(msg VARCHAR(255))
BEGIN
insert into _debugLogTable select msg;
END$$
CREATE FUNCTION `ValueMeetsCondition`(value varchar(20)) RETURNS tinyint(1)
BEGIN
DECLARE ConditionValue INTEGER;
call debug_msg('begin !!!');
SET ConditionValue = CAST(`value` AS UNSIGNED);
call debug_msg('end !!!');
RETURN TRUE;
END$$
DELIMITER ;
I am aware that CAST function fails, but why call debug_msg('begin !!!'); does not insert new record into table?! There are not any transactions there!
Just want post an answer, maybe it will help somebody in the future.
From this we have -
If autocommit mode is enabled, each SQL statement forms a single transaction on its own. By default, MySQL starts the session for each new connection with autocommit enabled, so MySQL does a commit after each SQL statement if that statement did not return an error. If a statement returns an error, the commit or rollback behavior depends on the error
I call function in this way - select ValueMeetsCondition('>10').
So actually it is wrapped into transaction by MySQL, that's why if something inside my procedure fails - the whole changes are roll backed.
If i remake my query in this way the message begin !! will be inserted, while end !! does not
call debug_msg('begin !!!');
SET ConditionValue = CAST(`>10` AS UNSIGNED);
select ConditionValue;
call debug_msg('end !!!');
So here's the stored procedure I've written. When I ran the DELETE and UPDATE in a single sql tab
as:
DELETE FROM curriculumsubjects WHERE curriculumId = 27;
INSERT INTO curriculumsubjects(curriculumId,subjectCode)
VALUES(27,'MATH101');
it works. It executes delete and insert without any problem
But if I call the stored procedure as:
CALL `enrollmentdb`.`updateCurriculumSubjects`(27, 'MATH101');
it returns the 'error' string i put during ROLLBACK
What could be causing the failure of transaction within the stored procedure body when it runs successfully if ran without stored procedure CALL?
Here's the stored procedure.
CREATE DEFINER=`root`#`localhost` PROCEDURE `updateCurriculumSubjects`(IN p_curriculumId int, IN p_subjectCode varchar(100))
BEGIN
DECLARE hasError BOOLEAN DEFAULT 0;
DECLARE CONTINUE HANDLER FOR sqlexception SET hasError = 1;
START TRANSACTION;
DELETE FROM curriculumsubjects WHERE curriculumId = p_curriculumId;
INSERT INTO curriculumsubjects(curriculumId,subjectCode)
VALUES(p_curriculumId,p_subjCode);
IF hasError THEN
ROLLBACK;
SELECT 'error';
ELSE
COMMIT;
END IF;
END
By the way I'm using Mysql Workbench 6.3 and what I'm trying to do is to delete all the columns matching the curriculumId before I insert again.
On Java, I'll be iterating the call to the stored procedure for multiple inserts.
I hope you can help. I just can't find a reason why delete and insert won't work if put within a transaction.
Thanks.
You have just a mistake in your insert syntax which you have written p_subjCode but your input variable is p_subjectCode and I change Boolean type variable to tinyint(1) for more version support.
CREATE DEFINER=`root`#`localhost` PROCEDURE `updateCurriculumSubjects`(IN p_curriculumId int, IN p_subjectCode varchar(100))
BEGIN
DECLARE hasError TINYINT(1) DEFAULT 0;
DECLARE CONTINUE HANDLER FOR sqlexception SET hasError = 1;
START TRANSACTION;
DELETE FROM curriculumsubjects WHERE curriculumId = p_curriculumId;
INSERT INTO curriculumsubjects(curriculumId,subjectCode)
VALUES(p_curriculumId,p_subjectCode);
IF hasError=1 THEN
ROLLBACK;
SELECT 'error';
ELSE
COMMIT;
END IF;
END
Straight from the manual, here's the canonical example of merge_db in PostgreSQL:
CREATE TABLE db (a INT PRIMARY KEY, b TEXT);
CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
LOOP
-- first try to update the key
UPDATE db SET b = data WHERE a = key;
IF found THEN
RETURN;
END IF;
-- not there, so try to insert the key
-- if someone else inserts the same key concurrently,
-- we could get a unique-key failure
BEGIN
INSERT INTO db(a,b) VALUES (key, data);
RETURN;
EXCEPTION WHEN unique_violation THEN
-- Do nothing, and loop to try the UPDATE again.
END;
END LOOP;
END;
$$
LANGUAGE plpgsql;
SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');
Can this be expressed as a user-defined function in MySQL, and if so, how? Would there be any advantage over MySQL's standard INSERT...ON DUPLICATE KEY UPDATE?
Note: I'm specifically looking for a user-defined function, not INSERT...ON DUPLICATE KEY UPDATE.
Tested on MySQL 5.5.14.
CREATE TABLE db (a INT PRIMARY KEY, b TEXT);
DELIMITER //
CREATE PROCEDURE merge_db(k INT, data TEXT)
BEGIN
DECLARE done BOOLEAN;
REPEAT
BEGIN
-- If there is a unique key constraint error then
-- someone made a concurrent insert. Reset the sentinel
-- and try again.
DECLARE ER_DUP_UNIQUE CONDITION FOR 23000;
DECLARE CONTINUE HANDLER FOR ER_DUP_UNIQUE BEGIN
SET done = FALSE;
END;
SET done = TRUE;
SELECT COUNT(*) INTO #count FROM db WHERE a = k;
-- Race condition here. If a concurrent INSERT is made after
-- the SELECT but before the INSERT below we'll get a duplicate
-- key error. But the handler above will take care of that.
IF #count > 0 THEN
UPDATE db SET b = data WHERE a = k;
ELSE
INSERT INTO db (a, b) VALUES (k, data);
END IF;
END;
UNTIL done END REPEAT;
END//
DELIMITER ;
CALL merge_db(1, 'david');
CALL merge_db(1, 'dennis');
Some thoughts:
You can't do an update first and then check #ROW_COUNT() because it returns the number of rows actually changed. This could be 0 if the row already has the value you are trying to update.
Also, #ROW_COUNT() is not replication safe.
You could use REPLACE...INTO.
If using InnoDB or a table with transaction support you might be able to use SELECT...FOR UPDATE (untested).
I see no advantage to this solution over just using INSERT...ON DUPLICATE KEY UPDATE.
With regards to using MySQL stored procedures with transactions, and I am having a problem getting error output.
The problem is that I need to set an exit_handler to roll back the transaction if anything fails. But when I do this, I don't get any error output if something goes wrong. For example if I accidentally pass a NULL value and try to insert it into a non-null field.
I am using a return value to programmatically indicate success or failure, however this does nothing to tell me what actually went wrong.
I am using Perl DBI to talk to MySQL. I am using MySQL 5.0.92 on the production server and MySQL 5.0.51a on the development server. Upgrading to a newer version of MySQL is politically untenable.
This is a simplified example:
DELIMITER //
CREATE PROCEDURE pmt_new(
app_id varchar(40),
out ret tinyint unsigned,
out pmt_req_id int(10) unsigned)
BEGIN
DECLARE v_pmt_req_type int(10) unsigned;
DECLARE exit handler for not found, sqlwarning, sqlexception rollback;
set ret=1;
START TRANSACTION;
SELECT pmt_type INTO v_pmt_req_type FROM pmt_req_types WHERE pmt_req_name = 'Name 1';
INSERT INTO pmt_reqs (pmt_req_id, pmt_req_type, app_id)
values (null, v_pmt_req_type, app_id);
set pmt_req_id = last_insert_id();
INSERT INTO other (pmt_req_id) values (pmt_req_id);
COMMIT;
set ret=0;
END//
DELIMITER ;
Instead of just doing a rollback in your exit handler you need to return something as well...
You currently have
DECLARE exit handler for not found, sqlwarning, sqlexception rollback;
Change it to something like...
DECLARE exit handler for not found, sqlwarning, sqlexception
begin
rollback;
select "We had to rollback, error!";
end;
In 5.5 they added the SIGNAL/RESIGNAL statements so you could 'return' an error but prior versions you have to kind of roll your own solution. If you need you can declare multiple exit handlers to tailor the output better, or setup your own error table you can pull from.
You can also do input testing inside your stored procedure. Want to know if app_id is null?
DELIMITER //
CREATE PROCEDURE pmt_new(
app_id varchar(40),
out result varchar(256),
out ret tinyint unsigned,
out pmt_req_id int(10) unsigned)
BEGIN
DECLARE v_pmt_req_type int(10) unsigned;
DECLARE exit handler for not found, sqlwarning, sqlexception rollback;
SET ret=1;
SET result = "";
IF app_id IS NULL THEN
set result = "Supplied ID is Null";
ELSE
START TRANSACTION;
SELECT pmt_type INTO v_pmt_req_type FROM pmt_req_types WHERE pmt_req_name = 'Name 1';
INSERT INTO pmt_reqs (pmt_req_id, pmt_req_type, app_id)
values (null, v_pmt_req_type, app_id);
set pmt_req_id = last_insert_id();
INSERT INTO other (pmt_req_id) values (pmt_req_id);
COMMIT;
set ret=0;
END IF;
END//
DELIMITER ;
Doing it this way adds another out parameter, but gives you much better information. You could do the same with multiple exit handlers.