Prepared statement execution with variable number of parameters to be bound - mysql

Some stored procedures I work with need to interpolate WHERE criteria based on if procedure input parameters have been supplied. To avoid potential injection points, I'd like to utilize parameter binding for the values that are to be part of the interpolated criteria.
Since the criteria added to the prepared statement and thus the number of parameters to be bound may differ depending on the user input, I devised the method below to determine which variables will be passed to the EXECUTE statement. This works, but it seems inelegant.
CREATE PROCEDURE foo (IN mandatory INT, IN optional INT, IN optional2 VARCHAR(20))
BEGIN
SELECT
0, '', '', mandatory, optional, optional2
INTO
#params, #sql, #where, #m, #o1, #o2;
IF (#o1 > '' AND #o1 IS NOT NULL) THEN
SET #where = CONCAT(#where, ' AND field = ?');
SET #params = #params + 1;
END IF;
IF (#o2 > '' AND #o2 IS NOT NULL) THEN
SET #where = CONCAT(#where, ' AND field2 = ?');
SET #params = #params + 3;
END IF;
SET #sql = CONCAT('
SELECT id, bar FROM table
WHERE
baz = ?
', #where
);
PREPARE STMT FROM #sql;
CASE #params
WHEN 0 THEN EXECUTE STMT USING #m;
WHEN 1 THEN EXECUTE STMT USING #m, #o1;
WHEN 3 THEN EXECUTE STMT USING #m, #o2;
WHEN 4 THEN EXECUTE STMT USING #m, #o1, #o2;
END CASE;
DEALLOCATE PREPARE STMT;
END$$
I'm aware of alternatives:
The binaries that would call these stored procedures have a function that attempts to identify potential SQL injection by passing the user supplied strings through a regular expression.
A user-defined function could be used to dynamically construct the EXECUTE statement given a dynamic number of inputs.
However, I was wondering if anyone else has ran into this desire to handle dynamic construction of an EXECUTE statement purely with SQL.

However, I was wondering if anyone else has ran into this desire to handle dynamic construction of an EXECUTE statement purely with SQL.
Yes, me too.
Here's a PHP solution to generate the list of question marks for a prepared statement based on an array of unknown length:
/* My target query is this:
SELECT fun FROM fun_stuff WHERE fun_key IN ( ...unknown number of values... )
*/
/* For this example let's set our array to this: */
$val_arr = array(1,2,3,4,5,6,7,8,9);
$val_arr_cnt = count($val_arr); /* and count it */
/* Now make prepared statement q-mark string from values array */
$sql_prep = str_pad('?', ($val_arr_cnt * 2) - 1, ',?', STR_PAD_RIGHT);
/* add it to query */
$sql = "SELECT fun FROM fun_stuff WHERE fun_key IN ($sql_prep)";
/* And the result:
SELECT fun FROM fun_stuff WHERE fun_key IN (?,?,?,?,?,?,?,?,?)
*/
I've no idea how efficient this is. But I too every now and then want to implement the security and efficiency of MySQL prepared statements but have variable length input arrays

Not sure if it's possible to dynamically build the parameter list (changing the number of parameters on the fly, etc...). But since you can dynamically build your where clause, one very simple workaround is do something like this. Assuming your validations permit it, the else clause basically has the same effect as ignoring the parameter you may or may not be filtering on.
if p_cust_id is not null && p_cust_id > 0 then
set v_where_clause = concat(v_where_clause, ' c.cust_id = ? ');
set #v_cust_id := p_cust_id;
else
set v_where_clause = concat(v_where_clause, ' c.cust_id > ? ');
set #v_cust_id := 0;
end if;
then plug all the user variables above into your execute statement
execute str1 using #v_cust_id, #v_etc....;*

Related

How do I pull out specific strings from a comma separated string?

I have a string like so;
"field1=lance,field2=peter,field3=john"
The actual string has 20+fields, and I want to pull out specific values by name.
For instance, I want to pull out the value for "field2" and return the value "peter",
Can someone give me an elegant way of doing this in MySQL?
I should mention that this is a standard field format coming out of an eCommerce system. I have no control over the format. It would be possible to extract the data cleanly through the API, but that would be significant extra work, especially as I have the data already in this format.
interesting question. There is a lot below so let's break it down. We essentially build a query and execute the stmt
DELIMITER $$
DROP PROCEDURE IF EXISTS proc_loop_test$$
CREATE PROCEDURE proc_loop_test()
#create empty query string
set #sqlstring = '';
#set your string of fields, not sure where this comes from
set #mystring = 'field1=lance,field2=peter,field3=john';
#create number of times we will loop through the string
set #num = (select length(#mystring)
- length(replace('field1=lance,field2=peter,field3=john',',','')) +1);
#init loop
loop LOOP
#create a short string just taking the "last" field/value pair
set #shtstring = (select SUBSTRING_INDEX(#mystring,',',-1));
#recreate your query string removing the substring we created
set #mystring = (select left(replace(#mystring,#shtstring,''),length(replace(#mystring,#shtstring,''))-1));
#add to your query string, we will build this for each
set #sqlstring = concat(#sqlstring ,(select
concat('''',SUBSTRING_INDEX(#shtstring,'=',-1),''''
,' as ',
left(#shtstring,(position('=' in #shtstring) -1))) ),',');
#reduce our count by one as we have removed the latest field
set #num = #num - 1;
#leave the loop when no fields left
if #num = 0 then leave LOOP;
end if;
end loop LOOP;
END$$
DELIMITER ;
#create a query statement to execute
set #query = (select concat('select ',left(#sqlstring, length(#sqlstring)-1)));
#execute the query!
PREPARE stmt FROM #query;
EXECUTE stmt;
Result
field3 field2 field1
john peter lance
There is no array logic, this would be simple in presto SQL etc. Because you have an arbitrary number of fields being defined at any time we are going to need to loop, and unfortunately you cannot loop in mysql without creating a procedure
That is the first few lines. We also create our full string from your source and the number of iterations (number of fields in string).
Then basically we isolate the "last" field/value pair iterively, rearrange each one so field1=john turns into more sql friendly 'john' as field',
We reduce our counter and string each time we loop through until counter is 0. At which point we stop.
We then prepare a query with our value/field pairs and a 'select' string. Then execute and you get your values as fields
Credit
Dynamic Strings prepare/exec
Looping and stored procs
Simulating Split function
This answer from #Akina's comment works perfectly.
SUBSTRING_INDEX(SUBSTRING_INDEX(column, 'field2=', -1), ',', 1)
And WHERE accordingly.

Is there a better/alternative way to write a query that accepts an arithmetic operator than using dynamic SQL?

I'm writing a stored procedure where I have to update a quantity of specific product ( obviously I can either increase or decrease it). Since the only difference is an operator value( + or - ) I'd like to use the same procedure in both cases. I know this question was already answered here - Execute mathematical expression and set the value to variable in SQL , but it doesn't help me because they wrote it in MSSQL. Anyone knows how can I do it in MYSQL? Is it right , the following code is the best solution if I write in MYSQL ?
BEGIN
SET #foundID = -1,#name=name, #company=company;
SET #number=number, #action=plusMinus;
CALL spCheckIfProductExists(#name,#company,#foundID);
IF (#foundID != -1 AND (#action='+' OR #action='-'))
THEN SET #sql = concat("UPDATE instock SET quantity = quantity",#action , " #number WHERE productid= #foundID");
PREPARE update_statement FROM #sql;
EXECUTE update_statement;
DEALLOCATE PREPARE update_statement;
END IF;
END
This is a working code but I know it's a very bad practice to concatenate strings when I write dynamic stored procedure. Any ideas how to improve it ?
You can change sign of #number variable by #action value
BEGIN
SET #foundID = -1, #name = name, #company = company;
SET #action = plusMinus;
CALL spCheckIfProductExists(#name, #company, #foundID);
IF (#foundID != -1 AND (#action='+' OR #action='-')) THEN
IF (#action='-') THEN
SET #number = number;
ELSE
SET #number = -number;
END IF;
SET #sql = concat("UPDATE instock SET quantity = quantity + ? WHERE productid = ?");
PREPARE update_statement FROM #sql;
EXECUTE update_statement USING #number, #foundID;
DEALLOCATE PREPARE update_statement;
END IF;
END;
As long as it is controlled by a whitelist and is not used to fill a value context (which is better handled differently), it is “okay” to use dynamically generated SQL because the resulting SQL statement is controlled.
However, dynamic SQL is a large hammer that adds unnecessary complexity here.
In the “-” action case, simply multiply the value by -1 (read: change the sign) before using it in a placeholder or variable context and then always use + as the action: “quantity + x” or “quantity + (-x)” are both valid. This special-case would not apply to other operators such as “*”.
SET #adjustment = CASE WHEN #action = '-' THEN -#number ELSE #number END
UPDATE instock SET quantity = quantity + #adjustment
WHERE productid = #foundID
Even when allowing other operators, this could still be done with a ‘CASE #action WHEN ..’ and eliminate the dynamic SQL generation, as MySQL itself would perform different processing based on the action, and the action itself is only specified where a value is allowed.
UPDATE instock
SET quantity = (CASE #action
WHEN '*' THEN quantity * #number
WHEN '-' THEN quantity - #number
WHEN 'e' THEN 2.71828
ELSE quantity + #number
END)
WHERE productid = #foundID
Building SQL strings dynamically is generally only required when the shape of the query changes and, in many cases, approaching the query in an problem-specific manner often leads to cleaner solutions..

MySQL Prepared Statements Appeding Where Condition Not Working

Here is the actual query
select taken_date, DATE_FORMAT(taken_date, '%Y') taken_date_year, count(id) num_of_orders, sum(total_order_days) total_work_days from
(
select id, taken_date, getNumOfWorkDaysForOrder(order.order_num) total_order_days from order
where order.is_active = 1 and order.deleted_at is null and order.vendor_id = vendor_input and
order.company_id = company_input and order.contact_id = contact_input and order.candidate_id = candidate_input
order by taken_date
) as order_years group by YEAR(taken_date) order by taken_date desc;
I want to add where condition based on the input if it is not null, tried prepared statements and concatenation to add the where condition to the query but no luck.
DELIMITER $$
CREATE PROCEDURE
getAllActiveOrdersGroupByTakenDate(vendor_input INT, company_input INT, contact_input INT, candidate_input INT)
BEGIN
SET #prepareQuery = "select id, taken_date, getNumOfWorkDaysForOrder(order.order_num) total_order_days from order
where order.vendor_id = "+ vendor_input +" and order.is_active = 1 and order.deleted_at is null";
IF company_input IS NOT NULL THEN
SET #prepareQuery = CONCAT(#prepareQuery, ' ', "and order.company_id = "+company_input);
END IF;
IF contact_input IS NOT NULL THEN
SET #prepareQuery = CONCAT(#prepareQuery, ' ', "and order.contact_id = "+contact_input);
END IF;
IF candidate_input IS NOT NULL THEN
SET #prepareQuery = CONCAT(#prepareQuery, ' ', "and order.candidate_id = "+candidate_input);
END IF;
SET #finalQueryPart1 = CONCAT("select taken_date, DATE_FORMAT(taken_date, '%Y') taken_date_year, count(id) num_of_orders, sum(total_order_days) total_work_days from
(", #prepareQuery);
SET #finalQuery = CONCAT(#finalQueryPart1, ") as order_years group by YEAR(taken_date) order by taken_date desc");
PREPARE stmt FROM #finalQuery;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END $$
DELIMITER ;
Can someone help me to achieve this?
Update: you had an issue with your CONCAT() syntax before you edited your question.
When you want to append content, you must assign it back to the original string. CONCAT() is a function that returns the concatenated string. It does not have any side-effect of modifying the variable you use as an argument.
WRONG:
CONCAT(#prepareQuery, ' ', "and order.company_id=company_input");
RIGHT:
SET #prepareQuery = CONCAT(#prepareQuery, ' ', "and order.company_id=company_input");
Also, I'm not sure if you can reference the procedure input parameters in these expressions.
Frankly, I hardly ever use stored procedures. MySQL's implementation of stored procedures sucks. It's inefficient, doesn't save compiled procedures, there's no debugger, there are no packages, and so on.
Mostly I just execute dynamic SQL from my applications. There you have debugging, code reuse, familiar string manipulation in a familiar language.
I understand that stored procedures are the tradition in Oracle and Microsoft SQL Server communities, but it's really better to avoid stored procedures in MySQL.

Where clause not working in stored procedure, when working outside of it

We built a piece of dynamic sql that generates a wide view from data in long format. Seen here:
CREATE PROCEDURE `selectPivotedTermpoints`(studyid varchar(300))
BEGIN
SET SESSION group_concat_max_len = 10000000;
SET #psql = NULL;
SET #finalSQL = NULL;
SET #StudyID = studyid;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT('SUM(CASE WHEN terminate = ''', REPLACE(Terminate,'''', ''''''), ''' THEN 1 ELSE 0 END) AS `', REPLACE(Terminate,'''', ''), '`')
) INTO #psql
FROM Dashboard
WHERE studyid = #StudyID
AND completion_status = 'terminate';
SET #finalSQL = CONCAT('
SELECT Sample_provider as Provider,
completion_status as `Status`,',
#psql,'
FROM Dashboard
WHERE studyid = ''', #StudyID, '''
AND completion_status = ''terminate''
GROUP BY Sample_provider');
SELECT #finalSQL;
PREPARE stmt FROM #finalSQL;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END
When the sql is run as a query,(from SET to DEALLOCATE)setting #StudyID manually, we return a table with only the columns for that specific study(distinct Terminate as columns for only that study), however when the query is turned into a stored procedure and run it is generating a table with columns for all studies(all distinct Terminate as columns).
It appears that the first where clause (in the select group_concat) is being ignored when run as a stored procedure, but this is not the case when run as a simple query.
Stored procedure call:
selectPivotedTermpoints('bhp_03a');
Does anyone know why this is the case and / or how I can correct the issue?
I helped someone with a similar issue recently in another question; it confused us for quite a while. Change the parameter name to something else, I am guessing that WHERE is using it instead of the field in the table.
(You might be able to get away with Dashboard.studyid as well, but changing the parameter name will cause less confusion; and I am not positive how the query in #finalSQL would behave either.)

how to configure the table at runtime in mysql query

people know that we can use if statement to configure a query in the select statement like this
select if(var=1,amount,amount/2) from mytable;
But what if I want to achieve something like this:
select amount from if(var=1,mytable1,mytable2);
Is there any way to configure the table at run time?
SELECT amount FROM mytable1 WHERE #var = 1
UNION
SELECT amount FROM mytable2 WHERE #var = 0
UPD: Here's what MySQL EXPLAIN looks like for the part of the query which has a condition evaluating to FALSE:
Note the Impossible WHERE part. MySQL recognizes that the expression in WHERE is constantly evaluating to FALSE, so it doesn't even try executing the query. Hence, no performance overhead when using this approach.
(Upgrading to an answer)
Where did var come from?
If it's a variable in another language, you could test it in that other language and then construct different SQL as appropriate:
$sql = "SELECT amount FROM " . ($var = 1 ? "mytable1" : "mytable2");
If it's a user variable in SQL, you could similarly use an IF statement around the two alternative SELECT statements:
DELIMITER ;;
IF #var = 1 THEN
SELECT amount FROM mytable1;
ELSE
SELECT amount FROM mytable2;
END IF;;
DELIMITER ;
If it's anything else (like a field from your tables), then your question doesn't make a great deal of sense.
Based on the mysql manual pages, it appears you cannot do this with the traditional syntax.
"User variables are intended to provide data values. They cannot be used directly in an SQL statement as an identifier or as part of an identifier, such as in contexts where a table or database name is expected, or as a reserved word such as SELECT."
- [http://dev.mysql.com/doc/refman/5.6/en/user-variables.html][1]
The exception to this is that you can assemble a prepared statement, but is probably not a better solution for most programming tasks. It would be better to leave the sql string generation to the language invoking mysql.
But, if you are doing this as part of a "sql only" task, like an import, this seems to be the approach you must take.
SET #s = CONCAT("SELECT * FROM ", if(true, "table1", "table2"), " LIMIT 1");
PREPARE stmt FROM #s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET #s = CONCAT("SELECT * FROM ", if(false, "table1", "table2"), " LIMIT 1");
PREPARE stmt FROM #s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;