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.
Related
I have a MySQL stored procedure (shown below) that's supposed to build a list of IDs from a table of hierarchically related records. I'm having to re-engineer an older stored procedure to switch from using a simple CONCAT function to GROUP_CONCAT because the former couldn't handle the sizes of the lists being generated (i.e., the lists are going well over the 1024 character limit of the CONCAT function).
DELIMITER $$
CREATE PROCEDURE SPTest (top_id INT)
BEGIN
DECLARE ids_all TEXT;
SET SESSION group_concat_max_len = 1000000;
SET ids_all = top_id;
SET #str = GROUP_CONCAT('SELECT GROUP_CONCAT(id SEPARATOR \', \') ',
'FROM tbl WHERE nha_id = ', top_id, ' INTO #ids_tmp' SEPARATOR '');
PREPARE stmt FROM #str;
EXECUTE stmt;
WHILE #ids_tmp != "" DO
SET ids_all = GROUP_CONCAT(ids_all, #ids_tmp SEPARATOR ', ');
SET #str = GROUP_CONCAT('SELECT GROUP_CONCAT(id SEPARATOR \', \') ',
'FROM tbl WHERE nha_id IN (', #ids_tmp, ') INTO #ids_tmp' SEPARATOR '');
PREPARE stmt FROM #str;
EXECUTE stmt;
END WHILE;
SELECT ids_all AS ids;
END $$
DELIMITER ;
The problem is this routine is generating the following error when I try to call it (and sometimes it returns this error when I try to create the Stored Procedure):
ERROR 1111 (HY000): Invalid use of group function
When I manually create the same kinds of queries this code would build and run them at the command-line, they work perfectly fine -- no errors of any kind, and I get the results I expect. I've seen posts (both here on Stack Exchange and elsewhere) which say that MySQL doesn't support nested aggregate functions, but the concatenation here is just being done to strings. So I thought that maybe somehow it was seeing the GROUP_CONCAT in the string and burping because of that, so I tried putting "XXXX" in place of "GROUP_CONCAT" in the string and then using the REPLACE function to switch it back, but that made no difference. I also tried it with WHERE and HAVING for the criteria clause, but neither one worked. I've also done an extensive web search and was unable to find anything that was in any way helpful.
I don't know what else to try, mainly because I can't see what's wrong with the syntax here. Any help would be appreciated.
Update 1:
I have since tried a modified version of the script where the GROUP_CONCAT merges data from a subquery like this:
SET #qrystr = GROUP_CONCAT('SELECT GROUP_CONCAT(c SEPARATOR ", ") ',
'FROM (SELECT id AS c FROM tbl WHERE nha_id IN (', #ids_tmp,
') AS d INTO #ids_tmp' SEPARATOR '');
but that made no difference either.
You can't use GROUP_CONCAT() as a scalar function; it must be used in the context of a set of rows. Similarly, you can't use any other aggregate function without a table reference:
SET #x = MAX(<expr>); -- makes no sense
Ideally you should upgrade to MySQL 8.0 if you haven't already, and use a recursive CTE query instead:
WITH RECURSIVE hierarchy AS (
SELECT top_id AS id
UNION
SELECT tbl.id FROM tbl JOIN hierarchy ON tbl.nha_id = hierarchy.id
)
SELECT GROUP_CONCAT(id SEPARATOR ', ') AS ids_all FROM hierarchy;
That would eliminate the need for using loops or prepare/execute or temp variables.
Thanks to Bill Karwin's answer regarding the need to use SELECT with GROUP_CONCAT, I was able to see how my code needed to be changed. Rather than using a SET statement to assign the values to the variables, I need to use a SELECT ... INTO ... construct, as shown here:
DELIMITER $$
CREATE PROCEDURE ASSEMBLY_LIST_TEST (top_id INT)
BEGIN
DECLARE ids_all TEXT DEFAULT '';
SET SESSION group_concat_max_len = 1000000;
SET ids_all = top_id;
# Find the 1st-level children of 'top_id' to start building the list
SELECT GROUP_CONCAT('SELECT GROUP_CONCAT(id SEPARATOR ", ") FROM tbl ',
'WHERE nha_id = ', top_id, ' INTO #ids_tmp' SEPARATOR '') INTO #qrystr;
PREPARE stmt FROM #qrystr;
EXECUTE stmt;
# Recursively find the children of each level of children of the previous loop & add to the list
WHILE #ids_tmp != '' DO
SELECT GROUP_CONCAT(ids_all, ', ', #ids_tmp SEPARATOR '') INTO ids_all;
SELECT GROUP_CONCAT('SELECT GROUP_CONCAT(id SEPARATOR ", ") FROM tbl ',
'WHERE nha_id IN (', #ids_tmp, ') INTO #ids_tmp' SEPARATOR '') INTO #qrystr;
PREPARE stmt FROM #qrystr;
EXECUTE stmt;
END WHILE;
SELECT ids_all AS ids;
END $$
DELIMITER ;
This now produces exactly the result I was looking for. Of course, the recursive query approach is a much better solution for what I want this to do, but maybe my solution will help someone else.
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.
The following is the stored function:
DELIMITER //
CREATE FUNCTION `calcMedian`(
`tbl` VARCHAR(64),
`clm` VARCHAR(64)
) RETURNS decimal(14,4)
BEGIN
SELECT AVG(middle_values) AS 'median'
INTO medRslt
FROM (
SELECT t1.clm AS 'middle_values'
FROM
(
SELECT #row:=#row+1 as `row`, table_column_name
FROM tbl, (SELECT #row:=0) AS r
ORDER BY clm
) AS t1,
(
SELECT COUNT(*) as 'count'
FROM tbl
) AS t2
WHERE t1.row >= t2.count/2 and t1.row <= ((t2.count/2) +1)) AS t3;
RETURN medRslt;
END//
DELIMITER ;
I then proceed to execute the following query:
USE ap2;
SELECT vendor_id, calcMedian('invoices', 'invoice_total')
FROM invoices i
WHERE vendor_id = 97
GROUP BY vendor_id;
I get the error message:
SQL Error (1146): Table 'ap2.tbl' doesn't exist *
I understand that the following may be better off as stored procedure/prepared statement rather than function. I just want to take things one step at a time now.
Also I made a different function to simply output the value stored in the variable 'tbl', and it displayed the correct table name (invoices in this case).
Identifiers in a SQL statement cannot be provided as values. Identifiers (table names, column names, function names, etc.) must be specified in the SQL text.
To get the value of the tbl variable (procedure argument) used as a table name within a SQL statement in the procedure, you can use dynamic SQL.
Set a variable to the SQL text, incorporate the string value, and then execute the string as a SQL statement. As an example:
SET #sql = CONCAT( 'SELECT AVG(middle_values) AS `median`'
, ' INTO medRslt'
, ' ... '
, tbl
, ' ... '
);
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
Be aware that incorporating string values into the SQL text makes the procedure subject to SQL Injection vulnerabilities.
If I had to do this, I would reduce the potential for SQL Injection by verifying that tbl does not contain a backtick character, and enclose/escape the identifier in backticks, e.g.
CONCAT( ' ...' , '`' , tbl , '`' , ' ... ' );
^^^ ^^^
I have a prepared statement which should update an field.
CREATE PROCEDURE `update_table` (in id INT, in col nvarchar(11), in val nvarchar(10))
BEGIN
SET #sql = concat('UPDATE table SET ', col, ' = ', val , ' WHERE id = ', id);
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END
If I call the procedure with a string containing a hyphen (e.g. A-B)
CALL update_table(1, 'reg', 'A-B');
I get
Error 1054: Unknown column 'A' in 'field list'
Can you please assist in solving the issue?
Edit: I just figuered out the hyphen is not the cause of error. If I try to update with 'AB', the same error comes up.
The field to be updated is nvarchar as well with the same field length.
You're vulnerable to sql injection attacks, basically. Your sproc generated this sql:
UPDATE ... WHERE reg = A-B
Note the lack of quotes around A-B. You're not storing the string A-B in the reg field. You're doing mathematical subtraction: reg = A minus B, and neither A nor B are fields that exist in your table.
At BARE minimum you'd need:
SET #sql = concat('UPDATE table SET ', col, ' = "', val , '" WHERE id = ', id);
^----------^
so you're generating
UPDATE ... reg = "A-B"
I have this value in my MYSQL table "Test " (=="Test")
this text is basically in decimal format.
more info: https://en.wikipedia.org/wiki/T#Computing_codes
i am trying to run a select query as its a regular text.
these are the tests i'm running:
1.
query:
select TRIM(Leading ',' FROM replace(trim(name),"&#",",")) from table where id = 1;
output:
84,101,115,116
2.
query:
select char(84,101,115,116);
output: Test
3.
query:
select char(TRIM(Leading ',' FROM replace(trim(name),"&#",","))) from table where id = 1;
output: T
How can i get the whole word instead of just the first character?
Thanks,
Nir.
you are not getting expected result because
TRIM(Leading ',' FROM replace(trim(name),"&#",","))
returns string instead of array
'84,101,115,116'
Then char() only interprets first readable set of numbers.
If you want to continue, take a look at similar question made with procedures.
You should create a procedure that would return string decimals as a table result set and then use it as input to your CHAR(). This way you would move complicated logic to procedure and then use it like this (untested):
select
GROUP_CONCAT(
SELECT
char(*)
FROM
(
CALL database.getDecimalText()
)
) SEPARATOR '' )
from
table
where
id = 1;
As stated by Mulcek, your passing what looks like comma separated arguments to the CHAR function, but this is in fact a single string that contains commas. MySQL interprets it as such and returns the decoded first number it recognises.
A one step solution would be to use a prepared statement to build the whole query as a string and then execute that as follows:
SET #VTestVar = 'Test';
SET #VExec = CONCAT('SELECT CHAR(', (SELECT TRIM(Leading ',' FROM REPLACE(TRIM(#VTestVar),"&#",","))), ') value;');
PREPARE stmt FROM #VExec;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
Here it is adjusted for your use case:
SET #VTestVar = 'Test';
SET #VExec = CONCAT('SELECT CHAR(', (SELECT TRIM(Leading ',' FROM REPLACE(TRIM(select TRIM(Leading ',' FROM replace(trim(name),"&#",",")) from table where id = 1),"&#",","))), ') value;');
PREPARE stmt FROM #VExec;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
Regards,
James