Composing a dynamic query in a stored procedure in MYSQL - mysql

I am creating a stored procedure in MySQL in order to execute the same queries in several parts of my application.
One of the two tables, as you can guess from the code, will be different depending on the script that will execute the stored procedure. The fields of the second query will be the same in number and type. The name of the table and the name of a column will change.
In order to make a dynamic query, I have used the CONCAT() command but I don't like it very much because it contains too many fragmented parts and too many quotes. Is there a more elegant way to compose a dynamic query like this?
BEGIN
INSERT IGNORE INTO tag (cod, tag) values (cod_tag_in, tag_in);
SET #query = CONCAT("INSERT INTO ", table_in, " (cod, ", table_field_in, ", tag_cod) values ('", app_cod_in, "','", section_cod_in, "', (SELECT cod FROM tag WHERE tag = '", tag_in, "'))");
PREPARE stmt FROM #query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END

Here is how I would write this routine if the table name and field name are really dynamic:
BEGIN
INSERT IGNORE INTO tag (cod, tag) values (cod_tag_in, tag_in);
SET #query = CONCAT('INSERT INTO `', table_in, '` (cod, `', table_field_in, '`, tag_cod) values (?, ?, ?)');
SET #cod = app_cod_in, #field = section_code_in, #tag_cod = cod_tag_in;
PREPARE stmt FROM #query;
EXECUTE stmt USING #cod, #field, #tag_cod;
DEALLOCATE PREPARE stmt;
END
Use query parameters so you don't have to use all those meticulous quotes for the values. This also protects you in case the values themselves contain quote characters.
I don't see the need for the subquery, since the value you just inserted in the the tag table should be the same as cod_tag_in anyway.
You do need the breaks in quotes for the dynamic table name and dynamic field name, because you can't use parameters as table or column identifiers.
I put back-ticks around the table and field name, just in case these identifiers require them (conflict with SQL reserved keywords, contain whitespace or punctuation, etc.).
However, you shouldn't even use dynamic SQL for this at all. You should use a CASE statement for the tables you need to support:
BEGIN
INSERT IGNORE INTO tag (cod, tag) values (cod_tag_in, tag_in);
CASE table_in
WHEN 'mytable1' THEN
INSERT INTO mytable1 (cod, myfield1, tag_cod) VALUES (app_cod_in, section_code_in, cod_tag_in);
WHEN 'mytable2' THEN
INSERT INTO mytable2 (cod, myfield2, tag_cod) VALUES (app_cod_in, section_code_in, cod_tag_in);
WHEN 'mytable3' THEN
INSERT INTO mytable3 (cod, myfield3, tag_cod) VALUES (app_cod_in, section_code_in, cod_tag_in);
ELSE
SIGNAL SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Unknown table';
END CASE;
END
This means you don't have to worry about CONCAT()-ing any fragments of SQL, and you don't have to worry about SQL injection risks, and you don't have to worry about table or column identifiers that have conflicting characters.
But you are limited to the tables you have coded in the CASE statement. If you are in the habit of creating new tables on the fly frequently, you'd have to replace the stored procedure with longer and longer CASE statements. But if you need to do that, I'd reconsider the design that so frequently requires new tables of similar structure.

Related

How to set a local list/tuple variable in mysql

Is there a way to do the following in mysql?
SET #studios = ('Disney', 'Warner Bros.', 'Fox');
SELECT * FROM movies WHERE provider IN #studios;
When I try doing the above I get the error:
Operand should contain 1 column(s)
The error is coming from your initial assignment. You cannot assign lists to variables.
The only way of doing this in MySQL is to either create a temp table to hold the values, and then do ... IN (SELECT someVal FROM thatTemp), or to dynamically create the query with the values directly in the query string.
Example temp table creation:
CREATE TEMPORARY TABLE `someTemp` ( someVal VARCHAR(16) );
INSERT INTO `someTemp` (someVal) VALUES ('a'), ('b'), ('c');
SELECT * FROM myTable WHERE myField IN (SELECT someVal FROM someTemp);
DELETE TEMPORARY TABLE `someTemp`;
Alternatively, there is also FIND_IN_SET, which could be used like this:
SET #list = 'a,b,c';
SELECT * FROM myTable WHERE FIND_IN_SET(myField, #list) <> 0;
but this method probably has extremely poor performance (and may not be useable if your "myField" values may contain commas).
It is not possible to set a tuple/list/array in a user-defined variable in MySQL. You can use Dynamic SQL for the same:
-- we use single quotes two times to escape it
SET #studios = '(''Disney'', ''Warner Bros.'', ''Fox'')';
-- generate the query string
SET #query = CONCAT('SELECT * FROM movies WHERE provider IN ', #studios);
-- prepare the query
PREPARE stmt FROM #query;
-- execute it
EXECUTE stmt;
-- deallocate it
DEALLOCATE PREPARE stmt;
You could concatenate your list to a string, and use FIND_IN_SET as your criteria. Might not be super efficient, but makes the code quite easy to read and maintain.
Looks like this:
SET #studios = CONCAT_WS(',',
'Disney',
'Warner Bros.',
'Fox'
);
SELECT * FROM movies
WHERE FIND_IN_SET(provider, #studios) <> 0;

Creating a MySQL stored procedure to update records

I'm converting all of my existing MSSQL databases and stored procedures am stuck on a new stored procedure where I need to update an existing record. The procedure gets called from a web form once a record has been inserted into the database and en email sent successfully (or at least passed off to the SMTP server)
I've had a working procedure in MSSQL for a long time but am trying to convert it to MySQL. I'm passing in 3 variables - a bit indicating the email got sent, a string indicating which SMTP server has been used to sent the email and a unique record id so I'll know what record to update. I'm also adding the date and time to another field to know when the procedure ran.
I've got the following but keep getting an error "#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 '' at line 7 - yet I don't see anything off at line 7 - at least to my eye.
The code I'm trying to use is:
CREATE PROCEDURE `sp_Test`(
`emailSent_In` BIGINT UNSIGNED,
`emailTransport_In` VARCHAR(100),
`formSecret_In` VARCHAR(32)
)
BEGIN
SET #`query` := CONCAT('UPDATE ',`tbl_JustSayThanks`,'
SET `emailSent` = `emailSent_In`,
`emailTransport` = ',`emailTransport_In`,',
`emailSentDate` = NOW()
WHERE `formSecret` = ', `formSecret_In`, '');
PREPARE `stmt` FROM #`query`;
EXECUTE `stmt`;
#`query` := NULL;
DEALLOCATE PREPARE `stmt`;
END//
DELIMITER ;
Just FYI, I'm using the CONCAT based on a previous answer I received from wchiquito and will be passing in the table name eventually. But, I wanted to get it to work on a simplified level before going there.
The following is wrong:
SET #`query` := CONCAT('UPDATE ',`tbl_JustSayThanks`,'
because you seem to be concatenating your SQL text with the value of tbl_JustSayThanks, but I think you mean to use the identifier itself. This should therefore be:
SET #`query` := CONCAT('UPDATE `tbl_JustSayThanks`',
The following is wrong:
`emailTransport` = ',`emailTransport_In`,',
because the variable is a VARCHAR but you don't quote it as a string literal in your SQL statement. It's easy to get mixed up with the multiple levels of quoting. It should be:
`emailTransport` = ''', `emailTransport_In`, ''',
The following is wrong for the same reason:
WHERE `formSecret` = ', `formSecret_In`, '');
it should be:
WHERE `formSecret` = ''', `formSecret_In`, '''');
This still suffers from SQL injection problems, unless you can guarantee that the input parameters are safe (which is not a good assumption). If you need to concatenate values into your SQL expressions, you should use the QUOTE() function to do escaping:
SET #query = CONCAT('
UPDATE tbl_JustSayThanks
SET emailSent = ', QUOTE(emailSent_In), '
emailTransport = ', QUOTE(emailTransport_In), '
emailSentDate = NOW()
WHERE formSecret = ', QUOTE(formSecret_In));
More comments:
You don't need to delimit every identifier with back-ticks, only those that conflict with SQL reserved words, or contain whitespace or punctuation or international characters. None of your identifiers you show require delimiting.
When you use prepared statements, you should use query parameters with the ? placeholders, intead of concatenating variables into the SQL string. You don't quote parameter placeholders in your SQL query. That way you won't run into hard-to-debug syntax errors like the ones you found.
Here's an example showing the fixes:
CREATE PROCEDURE sp_Test(
emailSent_In BIGINT UNSIGNED,
emailTransport_In VARCHAR(100),
formSecret_In VARCHAR(32)
)
BEGIN
SET #query = '
UPDATE tbl_JustSayThanks
SET emailSent = ?,
emailTransport = ?,
emailSentDate = NOW()
WHERE formSecret = ?';
SET #es = emailSent_In;
SET #et = emailTransport_In;
SET #fs = formSecret_In;
PREPARE stmt FROM #query;
EXECUTE stmt USING #es, #et, #fs;
DEALLOCATE PREPARE stmt;
END//
DELIMITER ;
Final comment:
Your example query has no dynamic syntax elements, only dynamic values. So you don't need to use a prepared statement at all.
This is how I'd really write the procedure:
CREATE PROCEDURE sp_Test(
emailSent_In BIGINT UNSIGNED,
emailTransport_In VARCHAR(100),
formSecret_In VARCHAR(32)
)
BEGIN
UPDATE tbl_JustSayThanks
SET emailSent = emailSent_In,
emailTransport = emailTransport_In,
emailSentDate = NOW()
WHERE formSecret = formSecret_In;
END//
DELIMITER ;
You should also be aware that MySQL stored procedures are greatly inferior to Microsoft SQL Server. MySQL doesn't keep compiled stored procedures, it doesn't support packages, it doesn't have a debugger... I recommend you do not use MySQL stored procedures. Use application code instead.

JSON_EXTRACT as VALUES for INSERT INTO?

I expect the following MySQL code:
SET #json= '{"1":{"name":"Name","value":"James Bowery","id":1,"type":"name","first":"James","middle":"","last":"Bowery"},"2":{"name":"Birthdate","value":"06\/23\/2017","id":2,"type":"date-time","date":"06\/23\/2017","time":"","unix":1498176000},"3":{"name":"Gender","value":"Male","value_raw":"Male","id":3,"type":"radio"},"4":{"name":"Ethnicity","value":"European","value_raw":"European","id":4,"type":"radio"},"5":{"name":"Email","value":"jabowery#emailservice.com","id":5,"type":"text"}}';
SET #array = JSON_EXTRACT( #json, '$."1".first','$."1".last','$."5".value','$."2".value','$."3".value','$."4".value');
INSERT INTO user (firstname,lastname,birthdate,gender,ethnicity,email) VALUES (#array);
To insert a row into the user table populated by the named fields extracted from the JSON. However, the INSERT yields a syntax error. What is the proper syntax?
Perhaps try this. Note that I have changed the date format to yyyy-MM-dd. You will need to work on converting this programatically to fit to MySQL. Also, when I ran this, the array had email before birthdate, so I changed the sequence of insert.
SET #json= '{"1":{"name":"Name","value":"James Bowery","id":1,"type":"name","first":"James","middle":"","last":"Bowery"},"2":{"name":"Birthdate","value":"06\/23\/2017","id":2,"type":"date-time","date":"2017-06-23","time":"","unix":1498176000},"3":{"name":"Gender","value":"Male","value_raw":"Male","id":3,"type":"radio"},"4":{"name":"Ethnicity","value":"European","value_raw":"European","id":4,"type":"radio"},"5":{"name":"Email","value":"jabowery#emailservice.com","id":5,"type":"text"}}';
SET #array = JSON_EXTRACT( #json, '$."1".first','$."1".last','$."5".value','$."2".value','$."3".value','$."4".value');
SET #queryString= CONCAT( 'INSERT INTO user (firstname,lastname,email,birthdate,gender,ethnicity) VALUES (',REPLACE(REPLACE(#array, '[',''),']',''),')');
PREPARE stmt FROM #queryString;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

How to handle where clause within mysql procedure?

Here is my procedure.
DELIMITER //
drop procedure if exists GetID;
CREATE PROCEDURE GetID(IN tb VARCHAR(255), in name2 varchar(255))
BEGIN
set #sel = concat( 'select id FROM ', tb, ' WHERE ename = ', name2);
prepare stmt from #sel;
execute stmt;
deallocate prepare stmt;
END //
DELIMITER ;
When I tried to execute the stored procedure by using GetID('city', 'ny'). I got an error
unknown column 'ny' in where clause ...
Here 'city' is the table name. What is wrong?
Assuming that name2 is a string parameter which to be compared with ename column of the passed table
Put quotes around name2 in the SQL:
set #sel = concat('select id FROM ', tb, ' WHERE ename = ''', name2,'''');
It's usually recommended not to use string concatenation to build SQL queries. Since you are hardcoding the column name in the query, it makes little sense to provide the table name "dynamically". But, if you must, use QUOTE to properly escape and quote the passed string.
set #sel = concat('select id FROM ', tb, ' WHERE ename = ', quote(name2));
Never concatenate strings directly into queries. It's bad enough that you're passing a table name in, unsanitized. That needs to be fixed, too, but one correct solution to your immediate issue is this:
set #sel = concat( 'select id FROM ', tb, ' WHERE ename = ', QUOTE(name2));
The QUOTE() function correctly and safely quotes and escapes the argument, and also handles null values correctly... and prevents a SQL injection vulnerability here.
See https://dev.mysql.com/doc/refman/5.7/en/string-functions.html#function_quote.

Using result of SQL Query as table name in mysql trigger

I've to write a trigger on my table which will perform the following functions.
Before Update on row, check price of Item
If price has changed from the last price, then select the table name, where to insert the item name, from another table having type of item and the associated table name.
Insert the item name in the selected table.
To put simply i've a table(TypeNameTable) having item categories and corresponding table names, if the price of item has changed then i've to get the table name from the TypeNameTable and insert the item name in the table, which is retrieved from TypeNameTable.
I'm not able to insert into table when I get the table names dynamically. Please suggest how to do it. Here's what I'm doing:
BEGIN
#declare countryTableName varchar(50);
declare itemPrice int;
declare itemTableName text;
IF (New.Price != Old.Price) THEN
SET countryTableName = (select `ItemManager`.`TypeNames`.`TypeTableName`
from `ItemManager`.`TypeNames`
where `ItemManager`.`TypeNames`.`ItemType` = NEW.ItemType);
INSERT INTO `ItemManager`.itemTableName
( `ItemName`, `ItemPrice`,
VALUES
( NEW.Name, New.Price );
END IF;
END$$
I get the error
ItemManager.itemTableName doesn't exists.
Answering my own question.
Figured out that using Dynamic SQL is not allowed in MySQL triggers . The restrictions are listed here.
However it's possible in Oracle where we can use PRAGMA AUTONOMOUS_TRANSACTION which executes the query in new context, and hence supports Dynamic SQL.
Example listed here at Point 27 .
You could CONCAT() your INSERT statement into a variable and execute that as PREPARED STATEMENT, someting like
...
SET #sql := CONCAT( 'INSERT INTO ', itemTableName, ' ... ' );
PREPARE stmt1 FROM #sql;
EXECUTE stmt1;
DEALLOCATE PREPARE stmt1;
...
afaik this is the only way to process dynamically generated SQL in stored routines and triggers.
If it is possible, I'd suggest you to change design a little. Instead of different tables you can create one table itemTable.
...
IF (New.Price != Old.Price) THEN
INSERT INTO `ItemManager`.`itemTable`
( `ItemName`, `ItemPrice`,
VALUES
( NEW.Name, New.Price );
END IF;
...
If there are different item properties, this table can be a parent table for specific child tables.