I wrote an SQL stored procedure, that's called trough an SqlDataSource element (in a ASP.NET / C# web application).
To be more precise the procedure is called from within a trigger of tableA bound on the AFTER INSERT event:
CREATE PROCEDURE `create_checklist`(IN _id_A INT(10))
BEGIN
DECLARE _mode CHAR(4);
DECLARE _id_default INT(10);
SELECT
mode
FROM
tableA
WHERE
id = _id_A INTO _mode;
SELECT
id
FROM
tab_doc_type
WHERE
def_value = 1 INTO _id_default;
# LOCK TABLES TableA WRITE;
# START TRANSACTION;
INSERT INTO checklist
(id_type, id_A, priority)
SELECT id_doc_type, _id_A, priority
FROM tab_checklist
WHERE A_mode = _mode;
UPDATE
checklist
SET
not_needed = 1
WHERE
(id_A = _id_A ) AND (id_type = _id_default);
# COMMIT;
# UNLOCK TABLES;
END
CREATE TRIGGER `tableA_AINS` AFTER INSERT ON `tableA` FOR EACH ROW
BEGIN
CALL create_checklist(NEW.id);
IF NEW.mode = 'X' OR NEW.mode = 'Y' THEN
CALL create_booking(NEW.id);
END IF;
END
that should create the checklist entries for an element of the tableA and then update one of the checlist entries with a flag (not_needed ) with a predefined value.
My SQL backend is a MySQL server.
This works in most of the cases but users reports that 'sometime' the procedure fails without errors being shown, the checklist is not filled up so I'm assuming that INSERT command is not executed.
I'm not able to reproduce this behaviour.
I've been notified from a user the value of an _id_A on which the procedure failed and triggering it 'manually' from the MySQL SQL Editor worked withtout problems.
I tried to enclose lines of codes 'writing' data in table with LOCK TABLES .. UNLOCK TABLES and/or START TRANSACTION ... COMMIT without success. In both cases it seems that stored procedure cannot use such commands.
Anyone here is able to locate a possible failure reason?
There's a way to fix this?
In short there's a way to call the stored procedure from within the trigger and avoid race condition so the checklist table is correctly populated every time?
Related
please how to ensure that all records in this stored Prodcedures is execute fine, and if insert is not working good dont delete anything .
my Procedure like :
i have to table data and data_archive and i want to insert data into data_archive and after this operation delete my data from table data
CREATE DEFINER=`Mybase`#`%` PROCEDURE `archive`()
BEGIN
INSERT `data_archive`
SELECT * FROM `data`;
DELETE FROM `data`;
END
this procedure is working fine but i want to ensure dont delete if if the insert did not work
thank you for helps
CREATE DEFINER=`Mybase`#`%` PROCEDURE `archive`()
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
END;
START TRANSACTION;
INSERT `data_archive` SELECT * FROM `data`;
DELETE FROM `data`;
COMMIT;
END
If both statements are executed successfully then the changes are committed.
If any error occures then the handler rollbacks all changes.
You may add SIGNAL or RESIGNAL statement into the handler for to report to the application that the operation fails.
Trying to create a transaction in phpmyadmin using the routine panel. I want to do an insert and an update:
START TRANSACTION;
INSERT INTO inventoryitems (item, quantity, userid)
VALUES(item, quantity, userid);
UPDATE users
SET cash = cash - (quantity * unitbuyprice);
COMMIT;
You can see the create/edit routine panel in the screen shot below:
Below is the error I get:
The following query has failed: "CREATE DEFINER=root#localhost PROCEDURE InsertInventoryItem(IN item VARCHAR(255), IN quantity INT, IN userid INT, IN unitbuyprice INT) NOT DETERMINISTIC NO SQL SQL SECURITY DEFINER START TRANSACTION; INSERT INTO inventoryitems (item, quantity, userid) VALUES(item, quantity, userid); UPDATE users SET cash = cash - (quantity * unitbuyprice); COMMIT;"
MySQL said: #1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'INSERT INTO inventoryitems (item, quantity, userid) VALUES(item, quantity, user' at line 3
If I remove the Start Transaction, Commit and either the insert or update then the procedure is fine. IE just a single statement works fine but multiple statements always gives an error.
What am I missing when I want to include multiple statements in a procedure.
I have tried with and without the semi colon delimiter.
This stuff just works with MS SQL. I have created Procedures with hundreds of statements inside before.
Cheers for the Help in advance.
I suggest you add BEGIN and END.
Also note:
A local variable should not have the same name as a table column. If an SQL statement ... contains a reference to a column and a declared local variable with the same name, MySQL currently interprets the reference as the name of a variable.
Reference: https://dev.mysql.com/doc/refman/5.7/en/local-variable-scope.html
If we implement control of transaction within the context of a stored program, we should probably also handle an error condition, and issue the rollback within the stored program. (Personally, I adhere to the school of thought that believes we should handle transaction context outside of the stored procedure.)
The procedure definition would look something like this:
DELIMITER $$
CREATE DEFINER=root#localhost PROCEDURE InsertInventoryItem(
IN as_item VARCHAR(255),
IN ai_quantity INT,
IN ai_userid INT,
IN ai_unitbuyprice INT
)
BEGIN
-- handle error conditions by issuing a ROLLBACK and exiting
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
EXIT PROCEDURE;
END;
START TRANSACTION ;
INSERT INTO inventoryitems (item, quantity, userid)
VALUES (as_item, ai_quantity, ai_userid) ;
UPDATE users u
SET u.cash = u.cash - (ai_quantity * ai_unitbuyprice)
WHERE u.userid = ai_userid ;
COMMIT ;
END$$
DELIMITER ;
--
Note that the update will assign a NULL to cash if either ai_quantity or ai_unitbuyprice is NULL. And we probably want a WHERE clause to limit the rows that will be updated. (Without the WHERE clause, the UPDATE statement will update all rows in the table.)
That's what the statements would look like if I wanted to create the procedure from a normal client, such as the mysql command line, or SQLyog.
MySQL syntax is significantly different than Transact-SQL (Microsoft SQL Server). We just have to deal with that.
As far as "this stuff just works with MS SQL", in all fairness, we should be careful to not conflate MySQL itself with the trouble prone idiot-syncracies of the phpMyAdmin client.
I have a stored procedure, internally I want to call another procedure that returns a record set, how do I get an navigate the record set returned by the stored procedure via the 'CALL' ?
[edit] I've been trying to use a TEMPORARY TABLE as suggested, but having problems:
DROP TEMPORARY TABLE IF EXISTS tbl_HeadOfDepts;
CREATE TEMPORARY TABLE tbl_HeadOfDepts (biDept_id tinyint(4))
INSERT INTO tbl_HeadOfDepts CALL rsHeadOfAnyDepartments(vcCompKey, biWho_id);
I need to use CALL because 'rsHeadOfAnyDepartments' is not a function, but this will not be accepted.
Work in progress, but what I have so far that is not accepted by editor:
BEGIN
#--
# Procedure:
# rsWhoCanIaccess
#
# Parameters:
# vcCompKey, the key corresponding to the company
# biWho_id, the id of the person to check access for
#
# Returns:
# recordset containing all the people this person can access
#--
DECLARE tiSuperUser tinyint(4);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1 #sqlstate = RETURNED_SQLSTATE,
#errno = MYSQL_ERRNO, #text = MESSAGE_TEXT;
CALL procLogError(vcCompKey, CONCAT("rsWhoCanIaccess: "
,#errno, " (", #sqlstate, "): ", #text));
END;
#Is this user a super user?
SELECT tiIsSuperUser(vcCompKey, biWho_id) INTO tiSuperUser;
SET tiSuperUser = 0;#Hack for testing
IF (tiSuperUser = 1) THEN
#The user is a superuser, return everyone in the company
SELECT
t1.biPerson_id
FROM
tbl_people t1
INNER JOIN
tbl_companies t2
ON
t1.biCompany_id=t2.biCompany_id
AND
t2.vcKey=vcCompKey;
ELSE
#User is not a superuser, is the user head of any departments?
DROP TEMPORARY TABLE IF EXISTS tbl_HeadOfDepts;
CREATE TEMPORARY TABLE tbl_HeadOfDepts (biDept_id tinyint(4))
INSERT INTO tbl_HeadOfDepts CALL rsHeadOfAnyDepartments(vcCompKey, biWho_id);
SELECT * FROM tbl_HeadOfDepts;
END IF;
END
No, stored procedures can produce result sets, but not consume them directly as output from inner calls to other stored procedures. The best that you can do performance-wise is to populate a non-temporary work table and use the results.
Depending on your software and the reality of multiple callers concurrently, you might need to include a session id concept with an auto_increment (AI) column in some control table. This would ensure that with concurrency, multiple callers are not stomping on each other's rows, thus making it non-viable.
How that session would work, at a high level, is the following. The inner stored proc would be handed an AI value (theSession) from the control table, use it to populate a safely segmented session in the work table, and return as an out parameter to the outer (calling) stored proc. That outer one could then safely use those rows, and clean up at the end (delete from workTable where sessionId=theSession).
Why do I suggest a non-temporary work table? To be clear, the work table would be non-temporary. First of all there is the hassle of getting the if exists drop to work. Most importantly, though, it is about performance. DDL calls for temporary table creation are not inexpensive. You will only believe this when you do performance testing to see what I mean. It may seem trivial, but in trivial operations, those DDL calls for creation could very well account for the lion share of the time necessary for the inner stored proc to complete.
internally I want to call another procedure that returns a record set,
In your inner procedure create a TEMPORARY TABLE and populate that temp table saying insert into your_temp_table select query. then you can use that same temp table in your outer query anywhere.
It can even be a normal table as well and need not be temporary table. Also make sure to DROP the table once your procedure computation done as clean-up.
That's wrong per your comment. You should do it like below (a sample code)
create procedure rsHeadOfAnyDepartments(vcCompKey varchar(10), biWho_id int)
as
begin
DROP TEMPORARY TABLE IF EXISTS tbl_HeadOfDepts;
CREATE TEMPORARY TABLE tbl_HeadOfDepts(col1 int, col2 varchar(10), col3 varchar(30));
INSERT INTO tbl_HeadOfDepts
SELECT col1, col2, col3
FROM tblTest;
end
I noticed a very interesting (and unexpected as well) thing yesterday. I was given a task (on production environment) to update three columns of TableA (I am changing the table and column names due to some obvious reasons) by getting all the values present in dummytable. The primary key of both the tables is column A. I know that this task was very simple and could be accomplished in several ways but I chose to write a stored procedure (given below) for that.
When the stored procedure was finished executing then it was noticed that columns B, C & statusCode were having the same values (i.e. thousands of records were having identical values in these three columns). Can someone tell me what went wrong?
1) What's wrong (or missing) in this stored procedure? (Dummy table had thousands of records as well)
2) What could be the best possible way of doing this task other than creating a stored procedure?
PS: I created (executed as well) this stored procedure on production environment using MySQL workbench and I got an exception during the execution of the procedure which stated something "Lost connection to MySQL server" but I guess since I was running this procedure on the remote machine then there was no interruption on the server while the procedure was executing.
Here is my stored procedure.
DELIMITER $$
CREATE DEFINER=`ABC`#`%` PROCEDURE `RetrieveExtractionData`()
BEGIN
DECLARE claimlisttraversed BOOLEAN DEFAULT FALSE;
DECLARE a VARCHAR(20);
DECLARE b INTEGER;
DECLARE c INTEGER;
DECLARE claimlist CURSOR FOR SELECT
`dummytable`.`A`,
`dummytable`.`B`,
`dummytable`.`C`
FROM `ABC`.`dummytable`;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET claimlisttraversed = TRUE;
OPEN claimlist;
claimlistloop: LOOP
FETCH claimlist INTO a, b, c;
IF claimlisttraversed THEN
CLOSE claimlist;
LEAVE claimlistloop;
END IF;
UPDATE `ABC`.`TableA`
SET
`B` = b,
`C` = c,
`statuscode` = 'Sent'
WHERE `A` = a;
END LOOP claimlistloop;
END
For your first question:
1) What's wrong (or missing) in this stored procedure? (Dummy table had
thousands of records as well)
I guess you forgot to CLOSE the CURSOR. Right after you end the LOOP, you should CLOSE the CURSOR.
END LOOP claimlistloop;
CLOSE claimlist;
END
2) What could be the best possible way of doing this task other than
creating a stored procedure?
Doing that in the STORED PROCEDURE should be fine. And also using CURSOR would be fine since you will just execute the procedure once (I guess because this is a production fix).
But from your question, you just want to update TableA based from the provided DummyTable. I assume that these tables have the same columns.
So I think this query is better than the CURSOR:
UPDATE TableA A
INNER JOIN DummyTable D ON D.A = A.A
SET A.B = D.B
, A.C = D.C
, A.statuscode = 'Sent';
But please try it first on a backup or dummy table. I haven't tested it yet.
Forget the cursor. In fact you should never use a cursor if it's avoidable. Cursors are incredibly slow.
Simply do
UPDATE
yourTable yt
INNER JOIN dummyTable dt ON yt.A = dt.A
SET
yt.B = dt.B,
yt.C = dt.C;
and you're fine.
1) What's wrong (or missing) in this stored procedure? (Dummy table
had thousands of records as well)
2) What could be the best possible
way of doing this task other than creating a stored procedure?
IMHO the most important thing that you're currently missing is that you don't need any cursors for that. Your whole stored procedure is one UPDATE statement. Execute it alone or wrap it in a stored procedure
CREATE PROCEDURE RetrieveExtractionData()
UPDATE TableA a JOIN dummytable d
ON a.a = d.a
SET a.b = d.b, a.c = d.c, a.statuscode = 'Sent';
You don't even need to change a delimiter and use BEGIN ... END block
Here is SQLFiddle demo.
I have a situation in which I don't want inserts to take place (the transaction should rollback) if a certain condition is met. I could write this logic in the application code, but say for some reason, it has to be written in MySQL itself (say clients written in different languages will be inserting into this MySQL InnoDB table) [that's a separate discussion].
Table definition:
CREATE TABLE table1(x int NOT NULL);
The trigger looks something like this:
CREATE TRIGGER t1 BEFORE INSERT ON table1
FOR EACH ROW
IF (condition) THEN
NEW.x = NULL;
END IF;
END;
I am guessing it could also be written as(untested):
CREATE TRIGGER t1 BEFORE INSERT ON table1
FOR EACH ROW
IF (condition) THEN
ROLLBACK;
END IF;
END;
But, this doesn't work:
CREATE TRIGGER t1 BEFORE INSERT ON table1 ROLLBACK;
You are guaranteed that:
Your DB will always be MySQL
Table type will always be InnoDB
That NOT NULL column will always stay the way it is
Question: Do you see anything objectionable in the 1st method?
From the trigger documentation:
The trigger cannot use statements that explicitly or implicitly begin or end a transaction such as START TRANSACTION, COMMIT, or ROLLBACK.
Your second option couldn't be created. However:
Failure of a trigger causes the statement to fail, so trigger failure also causes rollback.
So Eric's suggestion to use a query that is guaranteed to result in an error is the next option. However, MySQL doesn't have the ability to raise custom errors -- you'll have false positives to deal with. Encapsulating inside a stored procedure won't be any better, due to the lack of custom error handling...
If we knew more detail about what your condition is, it's possible it could be dealt with via a constraint.
Update
I've confirmed that though MySQL has CHECK constraint syntax, it's not enforced by any engine. If you lock down access to a table, you could handle limitation logic in a stored procedure. The following trigger won't work, because it is referencing the table being inserted to:
CREATE TRIGGER t1 BEFORE INSERT ON table1
FOR EACH ROW
DECLARE num INT;
SET num = (SELECT COUNT(t.col)
FROM your_table t
WHERE t.col = NEW.col);
IF (num > 100) THEN
SET NEW.col = 1/0;
END IF;
END;
..results in MySQL error 1235.
Have you tried raising an error to force a rollback? For example:
CREATE TRIGGER t1 BEFORE INSERT ON table1
FOR EACH ROW
IF (condition) THEN
SELECT 1/0 FROM table1 LIMIT 1
END IF;
END;