MySQL Stored Procedures with detail table and transactions - mysql

I have a relatively large (for me) stored procedure that I'm working on that is responsible for maintaining inventory values whenever a "movement" occurs.
I originally had all the checks and inserts happening in PHP, but I felt that the number of things that need to happen in the database made it more controllable by putting it in a stored procedure and denying INSERT/UPDATE permissions to these tables.
So history aside, I'm curious if there is a way to run a prepared statement for the stored procedure in which I can pass multiple values that will be insert into a detail table.
For example, let's say I want to insert the following data into a parent and children records.
"ACME", "TNT", 100
"ACME", "Anvil", 5
The parent record would contain ACME, and the children records would have the details of TNT, 100 and Anvil, 5.
Is it possible to somehow pass this data into a single stored procedure?
The reason I'm hoping to do this in one pass, is let's say we don't have enough TNT available, I don't want the original parent record to be inserted unless the entire transaction can take place.

Would concatenating the data sent to the stored procedure work? I don't imagine sending more than 1000 detail lines at a time to the database.
do_movement('ACME','2:TNT,100||Anvil,5'); The 2 in front denotes the number of details to expect, might help with error catching. Just a thought.
This way the code would be responsible for formatting, but I could output SQLEXCEPTIONS and have everything happen in one true transaction.
Drawing heavily from this post: MySQL Split Comma Separated String Into Temp Table
Using the SPLIT_STR function from (also referenced in previous post): http://blog.fedecarg.com/2009/02/22/mysql-split-string-function/
CREATE FUNCTION SPLIT_STR(
x VARCHAR(255),
delim VARCHAR(12),
pos INT
)
RETURNS VARCHAR(255)
RETURN REPLACE(SUBSTRING(SUBSTRING_INDEX(x, delim, pos),
LENGTH(SUBSTRING_INDEX(x, delim, pos -1)) + 1),
delim, '');
And then my functionality to parse N:s1|s2|..|sN and check.
I could easily call SPLIT_STR again if I had something like N:s1,a1|s2,a2|..|sN,aN
BEGIN
DECLARE defined_quantity INT;
IF serial_numbers REGEXP '^[^0]{0,}[[:digit:]]+:.*$' = TRUE THEN -- check that we are indeed at least starting properly formatted.
SET defined_quantity = CONVERT(LEFT(serial_numbers,LOCATE(':',serial_numbers)-1), UNSIGNED);
IF defined_quantity <= 0 THEN
SIGNAL SQLSTATE '45006'
SET MESSAGE_TEXT = 'The quantity defined with the serial number list is <= 0.';
END IF;
SET serial_numbers = RIGHT(serial_numbers,LENGTH(serial_numbers) - LOCATE(':',#serial_numbers));
BEGIN
DECLARE a INT Default 0 ;
DECLARE str VARCHAR(255);
DECLARE q INT;
simple_loop: LOOP
SET a=a+1;
SET str=TRIM(SPLIT_STR(serial_numbers,"|",a));
IF str='' THEN
SET a=a-1; -- we ignore the last increment
LEAVE simple_loop;
END IF;
#Do Inserts into temp table here with str going into the row
INSERT INTO transaction_detail (transaction_id, serial_number,created_at,updated_at) VALUES(transaction_id, str,NOW(),NOW());
END LOOP simple_loop;
SELECT a, defined_quantity, quantity;
IF a <> defined_quantity OR a <> quantity OR defined_quantity <> quantity THEN
SIGNAL SQLSTATE '45007'
SET MESSAGE_TEXT = 'The quantities do not match for the serial numbers provided.';
END IF;
END;
ELSE
SIGNAL SQLSTATE '45005'
SET MESSAGE_TEXT = 'The serial number formatted list is not properly formatted. Please provide in "n:s1|s2|s3|...|sn" format.';
END IF;
END;

Related

Want to generate unique Id's using a function in mysql

I wrote a function to generate unique id's,its working but sometimes two people are getting same id,I mean duplicates are formed. My unique id looks like
2016-17NLR250001, I deal with only last four digits 0001. I am posting my function please correct it and please help me in avoiding duplicates even though users login into same account or if they do it on same time.
MY FUNCTION:
DELIMITER $$
USE `olmsap`$$
DROP FUNCTION IF EXISTS `fun_generate_uniqueid`$$
CREATE DEFINER=`root`#`%` FUNCTION `fun_generate_uniqueid`( V_DATE DATE,V_MANDALID INT ) RETURNS VARCHAR(30) CHARSET latin1
DETERMINISTIC
BEGIN
DECLARE MDLCODE VARCHAR(5);
SET MDLCODE = ' ';
SELECT COUNT(*) INTO #CNT FROM `st_com_mandal` WHERE MANDAL_VS_MC=V_MANDALID;
SELECT dist_mandal_code INTO MDLCODE FROM `st_com_mandal` WHERE MANDAL_VS_MC=V_MANDALID;
IF #CNT>0 THEN
SET #YR=`FUN_FISCAL_YR`(V_DATE);
SELECT CONCAT(IF(DIST_SAN_CODE='GUN','GNT',DIST_SAN_CODE),IFNULL(`dist_mandal_code`,'NULL'))INTO #MANDAL
FROM `st_com_dist` SCD INNER JOIN `st_com_mandal` STM ON STM.`mandal_dist_id`= SCD.`DIST_VC_DC` WHERE MANDAL_VS_MC=V_MANDALID;
IF MDLCODE >0 THEN
SELECT COUNT(Soil_Sample_ID)+1 INTO #ID FROM `tt_mao_soil_sample_dtls` WHERE MANDAL_ID=V_MANDALID AND SUBSTR(UNIQUE_ID,1,7)=#YR ;
ELSE
SELECT COUNT(Soil_Sample_ID)+1 INTO #ID FROM `tt_mao_soil_sample_dtls` WHERE SUBSTR(UNIQUE_ID,1,14)=CONCAT(#YR,#MANDAL) ;
END IF ;
IF LENGTH(#ID)=1 THEN
SET #ID=CONCAT('000',#ID);
ELSEIF LENGTH(#ID)=2 THEN
SET #ID=CONCAT('00',#ID);
ELSEIF LENGTH(#ID)=3 THEN
SET #ID=CONCAT('0',#ID);
ELSE
SET #ID=#ID;
END IF ;
RETURN CONCAT(#YR,#MANDAL,#ID);
ELSE
RETURN 'Mandal Doesnt Exists';
END IF;
END$$
DELIMITER ;
I do not think community will be able to help you with this question. This is a complex function that requires very careful analysis of table / index access and locking.
The only thing I can recommend is to not use existing table data to calculate next sequence as this is a bad practice.
Besides Race conditions that you are experiencing you will also get problems if the record with the last sequence is deleted.
I suggest you read this to get an idea on how to write a custom sequence generator:
http://en.latindevelopers.com/ivancp/2012/custom-auto-increment-values/

Updating a column name of a same table which has a parent child relationship using mysql

I searched a lot of doing a task but found no appropriate solution.
Basically the scenario is. I have a user_comment table in which there are 5 column(id,parent_id,user_comments,is_deleted,modified_datetime). There is a parent child relationship like 1->2,1->3,2->4,2->5,5->7 etc. Now i am sending the id from the front end and i want to update the column is_deleted to 1 and modified_datetime on all the records on
this id as well as the all the children and children's of children.
I am trying to doing this by using a recursive procedure. Below is the code of my procedure
CREATE DEFINER=`root`#`localhost` PROCEDURE `user_comments`(
IN mode varchar(45),
IN comment_id int,
)
BEGIN
DECLARE p_id INT DEFAULT NULL ;
if(mode = 'delete')
then
update user_comment set is_deleted = 1, modified_datetime = now()
where id = comment_id ;
select id from user_comment where parent_id = comment_id into p_id ;
if p_id is not null
then
SET ##GLOBAL.max_sp_recursion_depth = 255;
SET ##session.max_sp_recursion_depth = 255;
call user_comments('delete', p_id);
end if;
end if;
END
By using this procedure it give me an error of more than one row.
If i return the select query without giving it to variable then shows me the the appropriate results on the select query but i have to call this procedure recursively based on getting the ids of the select query.
I need help i have already passed 2 days into this.
I used cursor also. Below is the code of cursor
CREATE DEFINER=`root`#`localhost` PROCEDURE `user_comments`(
IN mode varchar(45),
IN comment_id int,
)
BEGIN
DECLARE p_emp int;
DECLARE noMoreRow INT;
DECLARE cur_emp CURSOR FOR select id from user_comment where parent_id = comment_id ;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET noMoreRow = 0;
if(mode = 'delete')
then
OPEN cur_emp;
LOOPROWS: LOOP
IF noMoreRow = 0 THEN
update user_comment set is_deleted = 1, modified_datetime = now() where id = comment_id
CLOSE cur_emp;
LEAVE LOOPROWS;
END IF;
FETCH cur_emp INTO p_emp;
update user_comment set is_deleted = 1, modified_datetime = now() where id = p_emp ;
SET ##GLOBAL.max_sp_recursion_depth = 255;
SET ##session.max_sp_recursion_depth = 255;
call user_comments('delete', p_emp);
END LOOP;
end if;
END
After using cursor i am getting a thread error.i don't know how can overcome this problem!!!
Mysql's documentation on select ... into varlist clearly says:
The selected values are assigned to the variables. The number of
variables must match the number of columns. The query should return a
single row. If the query returns no rows, a warning with error code
1329 occurs (No data), and the variable values remain unchanged. If
the query returns multiple rows, error 1172 occurs (Result consisted
of more than one row). If it is possible that the statement may
retrieve multiple rows, you can use LIMIT 1 to limit the result set to
a single row.
Since you wrote in the OP that a comment can be parent of many comments, using simple variables cannot be a solution. You should use a CURSOR instead, that can store an entire resultset.
You loop through the records within the cursos as shown in the sample code in the above link and call user_comments() in a recursive way.
UPDATE
If your receive
Error Code: 1436. Thread stack overrun
error, then you can do 2 things:
Increase the thread_stack setting in the config file and restart mysql server.
You can try to simplify your code to use less recursions and therefore less stack space. For example, when you fetch all children into the cursor, then rather calling the user_comments() recursively for each, you can set all direct children's status within the code and call the function recirsively on grand-childrens only (if any). You can also change your data structure and use nested set model to approach hierarchical structures.
Nested set model is more complex to understand, it is less resource intensive to traverse, but more resource intensive to maintain.

Mysql: replace string in one table based on information on another table

I have the following problem, this is going to be long, I want to tell exactly all what I know about my problem in my question.
I have a table, field_body_value, with two fields, body_value and body_summary, containing strings of the form "/webfm_send/#" where # is a number.
I have another table called webfm_file where I have two fields with information for the string substitution: the first one is called fid, and it is the number # that I mentioned before, and the second is called fpatch, and gives me a string holding a path (for instance /data/html/files/file1.pdf) which has to substitute /webfm_send/# in the first table. The numbers # go up over the records of webfm_file but there are jumps, that is they increase but there are missing # so the final # is not equal to the number of records in webfm_file
So I thought the strategy was to set up a procedure which loops over the second table, and at each step of the sequence retrieves the pair fid/fpath, searches for "/webfm_send/fid" in the first table, and substitutes this by fpath in the first table.
So this is as far a I could arrive with my coding:
BEGIN
DECLARE v1 INT DEFAULT 0;
SELECT COUNT(*) INTO #numrec FROM `webfm_file`;
WHILE v1 < #numrec DO
SELECT fpath,fid INTO #path,#file FROM `webfm_file` LIMIT v1,1;
SET #webfm = concat('/webfm_send/',#file);
SET #cpath = concat('/',#path);
UPDATE `field_data_body`
SET body_value = replace(body_value, #webfm, #cpath),
body_summary = replace(body_summary, #webfm, #cpath)
WHERE body_value LIKE concat('%',#webfm,'%') OR
body_summary LIKE concat('%',#webfm,'%');
SET v1 = v1 + 1;
END WHILE;
END
Let me explain what I think I'm doing with the code above:
1) I retrieve the number of records in webfm_file for the loop.
2) The first SELECT gets a pair in fpath/fid from webfm_file, with LIMIT v1,1 I just check one record at a time, I checked an it works, the while loops over each record of webfm_file and the records are retrieved correctly.
3) The two next "set" fix the pair of strings #file/#path to create #webfm whith is the way its written in body_value at field_body_value, and to put a slash in front of #cpath which is the way I need this string to finally appear.
4) Then comes the UPDATE which will actually substitute the string if it finds it in either body_value or body_summary of field_body_data.
Expected: each instance of /webfm_send/# is substituted by the corresponding fpath pair of # (fid) in webfm_file
What I actually get: All appearances of /webfm_send/# no matter the value of # are substituted by the value of fpath in record 1 of webfm_file.
Things I have tried:
1) Take out the "WHERE" clause in the UPDATE sentence, which I believe is not strictly necessary since the replace function already takes care of finding a match but could speed up things. Same result
2) Resctrict the loop to loop just over a single record of webfm_file. Here it works in substituting the corresponding single retrieved pair fid/fpath, in the two instances of body_value and body_summary in field_body_data where fid=# appears in the string webfm_send/#
Thanks for following my explanation until here and thanks in advance for any hint.
You could use a cursor to iterate over the replacement strings. (There are faster ways using group_concat and it would be easier to do this in a general-purpose language rather than in a stored procedure). The general cursor approach would be:
drop procedure if exists proc;
delimiter //
create procedure proc()
begin
declare done boolean default 0;
declare path varchar(255);
declare id int;
declare cur cursor for select fpath, fid from webfm_file order by fid desc;
declare continue handler for sqlstate '02000' set done = 1;
open cur;
block: loop
fetch cur into path, id;
if done then
leave block;
end if;
set #from = concat('/webfm_send/', id);
update field_data_body set
body_value = replace(body_value, #from, path),
body_summary = replace(body_summary, #from, path);
end loop;
close cur;
end//
delimiter ;
call proc();

MySQL - creating a user-defined function for a custom sort

I'm working with a large set of legacy data (converted from a flat-file db), where a field is formatted as the last 2 digits of the year the record was entered, followed by a 4 digit increment...
e.g., the third record created in 1998 would be "980003", and the eleventh record created in 2004 would be "040011".
i can not change these values - they exist through their company, are registered with the state, clients, etc. I know it'd be great to separate out the year and the rest of it into separate columns, but that's not possible. i can't even really do it "internally" since each row has about 300 fields that are all sortable, and they're very used to working with this field as a record identifier.
so i'm trying to implement a MySQL UDF (for the first time) to sort. The query executes successfully, and it allows me to "select whatever from table order by custom_sort(whatever)", but the order is not what i'd expect.
Here's what I'm using:
DELIMITER //
CREATE FUNCTION custom_sort(id VARCHAR(8))
RETURNS INT
READS SQL DATA
DETERMINISTIC
BEGIN
DECLARE year VARCHAR(2);
DECLARE balance VARCHAR(6);
DECLARE stringValue VARCHAR(8);
SET year = SUBSTRING(0, 2, id);
SET balance = SUBSTRING(2, 6, id);
IF(year <= 96) THEN
SET stringValue = CONCAT('20', year, balance);
ELSE
SET stringValue = CONCAT('19', year, balance);
END IF;
RETURN CAST(stringValue as UNSIGNED);
END//
The records only go back to 96 (thus the arbitrary "if first 2 characters are less than 96, prepend '20' otherwise prepend '19'). I'm not thrilled with this bit, but don't believe that's where the core problem is.
To throw another wrench in the works, it turns out that 1996 and 1997 are both 5 digits, following the same pattern described above but instead of a 4 digit increment, it's a 3 digit increment. Again, I suspect this will be a problem, but is not the core problem.
An example of the returns I'm getting with this custom_sort:
001471
051047
080628
040285
110877
020867
090744
001537
051111
080692
040349
110941
020931
090808
001603
051175
I really have no idea what I'm doing here and have never used MySQL for a UDF like this - any help would be appreciated.
TYIA
/EDIT typo
/EDIT 2 concat needed "year" value added - still getting same results
You have some problems with your substrings, and the cast to int at the end makes it sort values with more digits at the end, not by year. This should work better;
DELIMITER //
CREATE FUNCTION custom_sort(id VARCHAR(8))
RETURNS VARCHAR(10)
READS SQL DATA
DETERMINISTIC
BEGIN
DECLARE year VARCHAR(2);
DECLARE balance VARCHAR(6);
DECLARE stringValue VARCHAR(10);
SET year = SUBSTRING(id, 1, 2);
SET balance = SUBSTRING(id, 3, 6);
IF(year <= 96) THEN
SET stringValue = CONCAT('20', year, balance);
ELSE
SET stringValue = CONCAT('19', year, balance);
END IF;
RETURN stringValue;
END//
DELIMITER ;
This can be simplified a bit to;
DELIMITER //
CREATE FUNCTION custom_sort(id VARCHAR(8))
RETURNS varchar(10)
DETERMINISTIC
BEGIN
IF(SUBSTRING(id, 1, 2) <= '96') THEN
RETURN CONCAT('20', id);
ELSE
RETURN CONCAT('19', id);
END IF;
END//
DELIMITER ;

How to insert multiple rows based on a quantity value in one row?

In MySQL, I am converting a table from a single row per item type (a quantity of items) to a single row per item, so that additional detail can be stored about individual items.
Here is an example source table:
id parent_id qty item_type
-- --------- --- ---------
1 10291 2 widget
2 10292 4 thinger
I want to create a new table with a new column containing info that cannot be applied to more than one item. Thus, the above table would end up as follows:
id parent_id item_type info
-- --------- --------- ----
1 10291 widget [NULL]
2 10291 widget [NULL]
3 10292 thinger [NULL]
4 10292 thinger [NULL]
5 10292 thinger [NULL]
6 10292 thinger [NULL]
Is there a way I can iterate or loop each row of the source table, inserting a number of records equal to the source qty column? I would prefer to do this in sql instead of code to keep all of the conversion steps together (there are many others).
You can do with stored procedure. That will be like below. Below is stored procedure I am using for inserting products into log based on their quantity.
Seem you have to do similar task. You can get how to use database cursor in stored procedure to loop over a result set in MySQL from below example.
DELIMITER $$
DROP PROCEDURE IF EXISTS CursorProc$$
CREATE PROCEDURE CursorProc()
BEGIN
DECLARE no_more_products, quantity_in_stock INT DEFAULT 0;
DECLARE prd_code VARCHAR(255);
DECLARE cur_product CURSOR FOR
SELECT productCode FROM products;
DECLARE CONTINUE HANDLER FOR NOT FOUND
SET no_more_products = 1;
/* for loggging information */
CREATE TABLE infologs (
Id int(11) NOT NULL AUTO_INCREMENT,
Msg varchar(255) NOT NULL,
PRIMARY KEY (Id)
);
OPEN cur_product;
FETCH cur_product INTO prd_code;
REPEAT
SELECT quantityInStock INTO quantity_in_stock
FROM products
WHERE productCode = prd_code;
IF quantity_in_stock < 100 THEN
INSERT INTO infologs(msg)
VALUES (prd_code);
END IF;
FETCH cur_product INTO prd_code;
UNTIL no_more_products = 1
END REPEAT;
CLOSE cur_product;
SELECT * FROM infologs;
DROP TABLE infologs;
END$$
DELIMITER;
Seems your task is 90% same as above procedure. Just do needful changes. It will work.
I think you can create stored procedure, declare a cursor that reads source table and for each row inserts qty rows into destination table.
Based on other answers which provided some insight, I was able to find additional information (by Kevin Bedell) to create a stored procedure and use a cursor in a loop. I have simplified my solution so that it matches the example in my question:
DROP PROCEDURE IF EXISTS proc_item_import;
DELIMITER $$
CREATE PROCEDURE proc_item_import()
BEGIN
# Declare variables to read records from the cursor
DECLARE parent_id_val INT(10) UNSIGNED;
DECLARE item_type_val INT(10) UNSIGNED;
DECLARE quantity_val INT(3);
# Declare variables for cursor and loop control
DECLARE no_more_rows BOOLEAN;
DECLARE item_qty INT DEFAULT 0;
# Declare the cursor
DECLARE item_cur CURSOR FOR
SELECT
i.parent_id, i.qty, i.item_type
FROM items i;
# Declare handlers for exceptions
DECLARE CONTINUE HANDLER FOR NOT FOUND
SET no_more_rows = TRUE;
# Open the cursor and loop through results
OPEN item_cur;
input_loop: LOOP
FETCH item_cur
INTO parent_id_val, item_type_val, quantity_val;
# Break out of the loop if there were no records or all have been processed
IF no_more_rows THEN
CLOSE item_cur;
LEAVE input_loop;
END IF;
SET item_qty = 0;
qty_loop: LOOP
INSERT INTO items_new
(parent_id, item_type)
SELECT
parent_id_val, item_type_val;
SET item_qty = item_qty + 1;
IF item_qty >= quantity_val THEN
LEAVE qty_loop;
END IF;
END LOOP qty_loop;
END LOOP input_loop;
END$$
DELIMITER ;
Before asking this question, I had not used a stored procedures, cursors, or loops. That said, I have read and encountered them frequently on SE and elsewhere, and this was a good opportunity to learn
It may be worth noting that the example on Kevin's page (linked above) does not use END%% (just END) which caused some headache in trying to get the script to work. When creating a procedure, it is necessary to change the delimiter temporarily so that semicolons terminate statements inside the procedure, but not the creation process of the procedure itself.
That is just an example of code that I have here, it is not adapted to your needs, but it does exactly what you need, and it is simple than a procedure, or temporary table.
SELECT event, id, order_ref, storeitem_barcode_create(8), NOW()
FROM (
SELECT mss.id, mss.event, mss.order_ref, mss.quantity, mss.product_id,
#rowID := IF(#lastProductID = mss.product_id AND #lastID = mss.id, #rowID + 1, 0) AS rowID,
#lastProductID := mss.product_id,
#lastID := mss.id
FROM module_barcode_generator mbg,
(SELECT #rowID := 0, #lastProductID := 0, #lastID := 0) t
INNER JOIN module_events_store_sold mss ON mss.order_ref = "L18T2P"
) tbl
WHERE rowId < quantity;
Typo in JYelton's solution for his/her own question:
FETCH item_cur
INTO parent_id_val, item_type_val, quantity_val;
Should be:
FETCH item_cur
INTO parent_id_val, quantity_val, item_type_val;
Otherwise very good.