MySQL stored procedure insert validation - mysql

I have two tables acceptances and turnovers. Let's just assume that these tables both have the columns id, and date only.
I need to be able to insert same dates across the 2 tables only up to 4 times.
For example: 5 users are trying to create a booking schedule.
3 of the 5 users create an acceptance schedule.
2 of the 5 users create a turnover schedule.
1 of these inserts should be rejected even in race conditions.
I have the following stored procedure:
BEGIN
DECLARE acceptanceDateEntryCount int;
DECLARE turnoverDateEntryCount int;
SELECT COUNT(date) FROM acceptances WHERE DATE(date) = DATE(insertDate) INTO acceptanceDateEntryCount;
SELECT COUNT(date) FROM turnovers WHERE DATE(date) = DATE(insertDate) INTO turnoverDateEntryCount;
IF((acceptanceDateEntryCount + turnoverDateEntryCount) < 4) THEN
INSERT INTO acceptances (date) VALUES (insertDate);
ELSE
SIGNAL SQLSTATE
'45030'
SET
MESSAGE_TEXT = 'Cannot create acceptance schedule. Max limit of 4 same date entries are found in acceptances table and turnovers table',
MYSQL_ERRNO = '45030';
END IF;
END
NOTE: This stored procedure is for creating an acceptance booking only. But there is another stored procedure which is just the same, but inserts into the turnovers table.
Is this stored procedure enough to make sure that even in race conditions where a lot of users try to create a booking schedule, if entries of the same date exists, the insert will be rejected if it has the same date.
Or should I just scrap this and use pessimistic locking to make sure that inserts on these tables do not run concurrently?

Tack FOR UPDATE on the end of each SELECT statement.
Also include
START TRANSACTION;
and
COMMIT;

Related

Mysql and execute stored procedure in atomic way or select update atomically

In Mysql I have two concurrent processes that need to read some rows and update a flag based on a condition.
I have to write a stored procedure with transaction but the problem is that sometimes the two processes updates the same rows.
I have a table Status and I want read 15 rows where the flag Reserved is true, then update those rows setting the flag Reserved to False.
The updated rows must be returned to the client.
My stored procedure is:
CREATE DEFINER=`user`#`%` PROCEDURE `get_reserved`()
BEGIN
DECLARE tmpProfilePageId bigint;
DECLARE finished INTEGER DEFAULT 0;
DECLARE curProfilePage CURSOR FOR
SELECT ProfilePageId
FROM Status
WHERE Reserved is false and ((timestampdiff(HOUR, UpdatedTime, NOW()) >= 23) or UpdatedTime is NULL)
ORDER BY UpdatedTime ASC
LIMIT 15;
DECLARE CONTINUE HANDLER
FOR NOT FOUND SET finished = 1;
DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK;
DECLARE EXIT HANDLER FOR SQLWARNING ROLLBACK;
START TRANSACTION;
DROP TEMPORARY TABLE IF EXISTS TmpAdsProfile;
CREATE TEMPORARY TABLE TmpAdsProfile(Id INT PRIMARY KEY AUTO_INCREMENT, ProfilePageId BIGINT);
OPEN curProfilePage;
getProfilePage: LOOP
FETCH curProfilePage INTO tmpProfilePageId;
IF finished = 1 THEN LEAVE getProfilePage;
END IF;
UPDATE StatusSET Reserved = true WHERE ProfilePageId = tmpProfilePageId;
INSERT INTO TmpAdsProfile (ProfilePageId) VALUES (tmpProfilePageId);
END LOOP getProfilePage;
CLOSE curProfilePage;
SELECT ProfilePageId FROM TmpAdsProfile;
COMMIT;
END
Anyway, if I execute two concurrent processes that call this stored procedure, sometimes they update the same rows.
How can I execute the stored procedure in an atomic way?
Simplify this a bit and use FOR UPDATE. That will lock the rows you want to change until you commit the transaction. You can get rid of the cursor entirely. Something like this, not debugged!
START TRANSACTION;
CREATE OR REPLACE TEMPORARY TABLE TmpAdsProfile AS
SELECT ProfilePageId
FROM Status
WHERE Reserved IS false
AND ((timestampdiff(HOUR, UpdatedTime, NOW()) >= 23) OR UpdatedTime IS NULL)
ORDER BY UpdatedTime ASC
LIMIT 15
FOR UPDATE;
UPDATE Status SET Reserved = true
WHERE ProfilePageId IN (SELECT ProfilePageId FROM TmpAdsProfile);
COMMIT;
SELECT ProfilePageId FROM TmpAdsProfile;
That temporary table will only ever have fifteen rows in it. So indexes and PKs and all that are not necessary. Therefore you can use CREATE ... AS SELECT ... to create and populate the table in one go.
And, consider recasting your UpdatedTime filter so it can use an index.
AND (UpdatedTime <= NOW() - INTERVAL 23 HOUR OR UpdatedTime IS NULL)
The appropriate index for the SELECT query is
CREATE INDEX status_update ON Status (Reserved, UpdatedTime, ProfilePageId);
The faster your SELECT operation can be, the less time your transaction will take, so the better your overall performance will be.

MySQL Stored Procedure - Checking if certain conditions are met one by one; if one is not met, exit procedure and return a specific message

I am trying to make a MySQL stored procedure that processes a book purchase and inserts records into other tables about the purchase. However, these insertions can only happen if three conditions are met: the customer is in the system, the book is in the system, and there is enough quantity.
I want to check for each condition individually, and if it passes the first condition, it moves to the next, but if it doesn't, I want it to end the procedure and return a value, and so on for each condition. If it passes all three conditions, the insertions can happen. Here's how I coded it:
DELIMITER //
CREATE PROCEDURE process_purchase(
IN book_key INT,
IN customer_key INT,
IN quantity INT
)
BEGIN
DECLARE book_inventory_key_var INT;
DECLARE purchase_key_var INT;
SELECT book_inventory_key
INTO book_inventory_key_var
FROM book_inventory
WHERE book_key = book_key.book_inventory_key;
SELECT purchase_key
INTO purchase_key_var
FROM purchases
WHERE customer_key = customer_key.purchases;
IF customer_key != customer_key.customers THEN
SELECT '-1';
ELSEIF book_key != book_key.books THEN
SELECT '-2';
ELSEIF quantity < quantity_on_stock(book_key) THEN
SELECT '-3';
ELSE
INSERT INTO purchases VALUES (customer_key, CURDATE());
INSERT INTO purchase_items (book_inventory_key, purchase_key, quantity) VALUES (book_inventory_key_var, purchase_key_var, quantity);
SELECT '1';
END IF;
END//
DELIMITER ;
I compare the customer and book keys to their values in the other tables, and the quantity to the quantity_on_stock stored function I previously made. I use a chain of IF-ELSEIF to go through each condition one by one, and if all of them are passed, the insertions occur. If not, it won't go to the next condition, and will return the SELECT message, correct? The procedure runs without errors, but I am unsure if this is the correct method, or if there's a better way of going about this.
Checking sequentially is subject to race conditions. Breaking this paradigm is key to moving from a procedural to SQL based method. Use the database features of to obtain consistency rather than procedural code.
purchase_items should have foreign key constraints to book_key and customer_key tables. If an insert generates a FK exception then one of these apply depending on the error. DECLARE HANDLER will help catch these errors.
For the quantity:
INSERT INTO purchase_items (book_inventory_key, purchase_key, quantity)
SELECT book_key, purchase_key, quantity
FROM books WHERE book_key = book.id AND book.available >= quantity
If there are no ROW_COUNT for this, then there wasn't sufficient quantity.
You will also need to reduce the amount of books available in the same SQL transaction.
If you don't have to do this in a STORED PROCEDURE, don't. A lot of the constructs here are easier in application code. If this is an assignment, where store procedures are require to get extra marks, get through it, and never write a stored procedure again.

How to use multiple sql statements in a mysql event?

These are the statements
INSERT INTO toolate (name,type,date)
SELECT name, type ,date
FROM homework
WHERE date < CURRENT_DATE()
and
DELETE FROM homework WHERE date < CURRENT_DATE()
I need to combine these two so that my event will work in a proper order.
Firstly the INSERT statement then the DELETE one.
That way I can still see homework that's past date while having a clean homework table and it needs to happen automatically thus why I'm using events. Of course I will welcome a different solution.
You can't combine these two in a single query. However, an alternative would be to use STORED PROCEDURE and execute these two inside a transaction with EXIT HANDLER e.g.:
BEGIN
START TRANSACTION;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
EXIT PROCEDURE;
END;
INSERT INTO toolate (name,type,date)
SELECT name, type ,date
FROM homework
WHERE date < CURRENT_DATE()
DELETE FROM homework WHERE date < CURRENT_DATE()
COMMIT;
END
This will make sure both of these queries are executed sequencially, and if DELETE query fails, INSERT will be rolled back.
Here's MtSQL's documentation for stored procedures.

sql refer row on masive insert using a stored procedures - mysql

I have a table task, this table contains information of recurring tasks, fox example daily tasks, so I repeat each task because I want to know the result of each task over time
1/1/2014 Get Pizza OK
1/2/2014 Get Pizza OK
1/3/2014 Get Pizza Error
1/4/2014 Get Pizza OK
For this I made a stored procedure
DELIMITER $$
CREATE DEFINER=`db`#`%` PROCEDURE `SP_repeat_task`()
BEGIN
INSERT INTO Task
(
date_of_assignment,
Some fields
)
SELECT
DATE_ADD(tas.date_of_assignment, INTERVAL 1 DAY),
another fields
FROM Task tas
WHERE tas.date_of_assignment=CURRENT_DATE and many conditions
)
;
END
This procedure is invoked every day 5 minutes before midnight. And produces something like this
The problem is that I have to insert the id of the records added in another table
For example
When I add 4 tasks in insert-select statement i need add these to another table
In my case, there can be multiple records for each task.
I can easily obtain id_person in my select, but not how to use it in the next insert.
I cant change the structure of the tables, I have only my stored procedure to work
I read about mysqli_insert_id, but not how to use it in my case
EDIT
based in b.b3rn4rd answer
When i add the other field in cursor select
DECLARE records_to_clone_cursor CURSOR FOR
SELECT `field1`, `field2`, `field3` FROM `Task` WHERE ... ;
In FETCH return more rows because as there is a one to many relationship in my tables
so the query returns with old fields
DECLARE records_to_clone_cursor CURSOR FOR
SELECT `field1`, `field2` FROM `Task` WHERE ... ;
And i tried change the prepared statement for a classic Insert-Select
SET #NEW_ID = LAST_INSERT_ID();
INSERT INTO Another_table
(
id_task,
id_person
)
SELECT tas.id_task,
pe.id_person
FROM Task tas
INNER JOIN Person pe
ON pe.id_person = tas.id_assigned
WHERE tas.id_task= #NEW_ID;
-- EXECUTE insert_responables USING #NEW_ID, #Var_3;
But does nothing, first prepared works well, and Select-Inser work in nornal query.
that I can do?
EDIT 2
if I need to insert the values​​, but because they are different cause the cursor query returns more records and these are duplicated by the number of records in the field is_person

MySQL transaction and triggers

Hey guys, here is one I am not able to figure out. We have a table in database, where PHP inserts records. I created a trigger to compute a value to be inserted as well. The computed value should be unique. However it happens from time to time that I have exact same number for few rows in the table. The number is combination of year, month and day and a number of the order for that day. I thought that single operation of insert is atomic and table is locked while transaction is in progress. I need the computed value to be unique...The server is version 5.0.88. Server is Linux CentOS 5 with dual core processor.
Here is the trigger:
CREATE TRIGGER bi_order_data BEFORE INSERT ON order_data
FOR EACH ROW BEGIN
SET NEW.auth_code = get_auth_code();
END;
Corresponding routine looks like this:
CREATE FUNCTION `get_auth_code`() RETURNS bigint(20)
BEGIN
DECLARE my_auth_code, acode BIGINT;
SELECT MAX(d.auth_code) INTO my_auth_code
FROM orders_data d
JOIN orders o ON (o.order_id = d.order_id)
WHERE DATE(NOW()) = DATE(o.date);
IF my_auth_code IS NULL THEN
SET acode = ((DATE_FORMAT(NOW(), "%y%m%d")) + 100000) * 10000 + 1;
ELSE
SET acode = my_auth_code + 1;
END IF;
RETURN acode;
END
I thought that single operation of
insert is atomic and table is locked
while transaction is in progress
Either table is locked (MyISAM is used) or records may be locked (InnoDB is used), not both.
Since you mentioned "transaction", I assume that InnoDB is in use.
One of InnoDB advantages is absence of table locks, so nothing will prevent many triggers' bodies to be executed simultaneously and produce the same result.