So I've written a fairly simple MySQL stored procedure to retrieve values from a database for a personal app that I'm building. From everything I can see, the procedure should work just fine, but it's returning the wrong results.
Here's the procedure code:
USE randyrip_kdb;
DROP PROCEDURE IF EXISTS spGetAllTracksSong;
DELIMITER //
CREATE PROCEDURE spGetAllTracksSong(IN startRecord INT, IN rowsReturned INT, IN searchArtist VARCHAR(255), IN searchTitle VARCHAR(244), IN orderBy VARCHAR(20), IN duets TINYINT(1))
BEGIN
DECLARE spStart INT;
DECLARE spRows INT;
DECLARE whereClause VARCHAR(255) DEFAULT '';
DECLARE whereArtist VARCHAR(255) DEFAULT '';
DECLARE whereSong VARCHAR(255) DEFAULT '';
DECLARE outputSQL VARCHAR(1000) DEFAULT '';
SET spStart=startRecord;
SET spRows=rowsReturned;
IF searchArtist!='' THEN SET whereArtist= CONCAT('artist LIKE \'%',searchArtist,'%\' '); END IF;
IF searchTitle!='' THEN SET whereSong= CONCAT('song_title LIKE \'%',searchTitle,'%\' '); END IF;
IF whereArtist != '' && whereSong !='' THEN SET whereClause=CONCAT('WHERE ', whereArtist,'AND ',whereSong);
ELSEIF whereArtist !='' THEN SET whereClause= CONCAT('WHERE',whereArtist);
ELSE SET whereClause = CONCAT('WHERE',whereSong);
END IF;
IF duets=1 && whereClause !='' THEN SET whereClause=CONCAT(whereClause,' AND is_duet=1');
END IF;
SET orderBy = IFNULL(orderBy, 'song_title');
IF orderBy='date' THEN SET orderBy='date_added DESC'; END IF;
/*select whereClause;
select orderBy;
select startRecord;
select rowsReturned;*/
SET outputSQL=CONCAT('SELECT song_title, artist, comments, disc_number FROM track ', whereClause,'ORDER BY ' ,orderBy,' LIMIT ' ,spStart,',',spRows);
SELECT outputSQL;
SELECT song_title, artist, comments, disc_number FROM track whereClause ORDER BY orderBy LIMIT spStart,spRows;
END//
DELIMITER ;
I'm calling the Stored Procedure with these parameters:
call spGetAllTracksSong(0,20,'elvis costello','peace, love','date',0);
The variable outputSQL is correctly generating the query I want, and when I run it it's returning two rows as expected. However, the procedure itself is returning 20 rows, none of which match the criteria.
If anyone has any ideas as to what I'm doing incorrectly, that would be great. From all that I can see, everything should be fine however.
Randy,
if you use variables in the SQL query (like "FROM track whereClause"), you need to execute with EXECUTE, otherwise it will not be evaluated. Replace your last select with this:
set #sql = outputSQL;
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
Alternatively, you could try not to use dynamic SQL.
Related
I have a procedure that returns multiple rows, but separately. Please take a look at its result:
I causes some issues when I want to fetch the result in the code (backend side). Now I want to create a temporary table and insert all rows inside it and then return that temp table as the result of the stored procedure. How can I do that inside procedure?
Not sure it above idea is a good idea .. that's the only thing I can probably be useful to merge all rows all in one table as SP's result.
Here is my current procedure:
DELIMITER $$
CREATE DEFINER=`administrator`#`localhost` PROCEDURE `lending_ewallets_balance_in_merchant`(IN `user_id_param` BIGINT UNSIGNED, IN `business_id_param` INT UNSIGNED)
NO SQL
BEGIN
DECLARE dossier_id INT;
DECLARE query_string VARCHAR(255) DEFAULT '';
DECLARE cursor_List_isdone BOOLEAN DEFAULT FALSE;
DECLARE user_dossiers CURSOR FOR
Select ld.id, lwp.query_string
FROM lending_users_dossiers ld
JOIN lending_where_to_pays lwp ON ld.lending_where_to_pay_id = lwp.id
WHERE user_id = user_id_param
AND (ld.status = 'activated' OR ld.status = 'finished');
# 'finished' is for loans
DECLARE CONTINUE HANDLER FOR NOT FOUND SET cursor_List_isdone = TRUE;
Open user_dossiers;
loop_List: LOOP
FETCH user_dossiers INTO dossier_id, query_string;
IF cursor_List_isdone THEN
LEAVE loop_List;
END IF;
SET #qry = CONCAT(
"SELECT ld.id lending_dossier_id, ld.type, SUM(let.credit) balance
FROM lending_users_dossiers ld
JOIN lending_ewallet_transactions let
ON ld.id = let.lending_dossier_id
WHERE ld.id = ", dossier_id,
" AND ", business_id_param, " IN(", query_string, ")",
"GROUP BY ld.id, ld.type");
PREPARE stmt FROM #qry;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END LOOP loop_List;
Close user_dossiers;
END$$
DELIMITER ;
Noted that, the MySQL version I use is MySQL v8.0.20.
The logic should be something like this. Outside the loop create a temp table if not exists and delete the data from it:
CREATE TEMPORARY TABLE
IF NOT EXISTS
user_dossiers_tmp (your columns);
DELETE FROM user_dossiers_tmp;
In your loop:
INSERT INTO user_dossiers_tmp VALUES (your data);
After your loop:
SELECT * FROM user_dossiers_tmp;
END$$
Is it possible to pass input parameter into Cursor SELECT statement WHERE clause?
For some reason I think it isn't working.
I'm trying to pass _TAG and _ITEM_NAME into where clause.
DECLARE cursor_test cursor for SELECT itemid FROM items WHERE key_ LIKE "sometext_#_TAG_sometext_#_ITEM_NAME" AND STATUS = '0';
Here is the the Stored procedure:
DELIMITER //
CREATE PROCEDURE getSomething(IN _HOSTNAME VARCHAR(20), _TAG VARCHAR(20), _ITEM_NAME VARCHAR(50))
BEGIN
declare FINISHED BOOL default false;
DECLARE cursor_test cursor for SELECT itemid FROM items WHERE hostid = #_HOSTID AND key_ LIKE "sometext_#_TAG_sometext_#_ITEM_NAME" AND STATUS = '0';
DECLARE CONTINUE HANDLER for not found set FINISHED := true;
SET #HOSTNAME = _HOSTNAME;
PREPARE STMT1 FROM
"SELECT hostid INTO #_HOSTID FROM hosts WHERE NAME = ?";
EXECUTE STMT1 USING #HOSTNAME;
DEALLOCATE PREPARE STMT1;
open cursor_test;
SET #TOTAL_VALUE := 0;
loop_itemid: loop
fetch cursor_test into _ITEMID;
SELECT _ITEMID;
if FINISHED then
leave loop_itemid;
end if;
SET #TOTAL_VALUE := #TOTAL_VALUE + (SELECT value from history_uint WHERE itemid = _ITEMID ORDER BY clock DESC LIMIT 1);
end loop loop_itemid;
SELECT #TOTAL_VALUE;
close cursor_test;
END //
Thanks to akina's comment. Using CONCAT in where condition worked.
WHERE key_ LIKE CONCAT('sometext_', _TAG, '_sometext_', _ITEM_NAME)
I have table users, with fields: id, name, age, gender, country, city, comment. Fields maybe null. For example:
cursor.execute('select * from users where id = 12')
cursor.fetchone()
(12, 'alex', 33, 'male', None, None, None)
How I can get back from query only not null fields?
This query must return me just
(12, 'alex', 33, 'male')
I can easily do it with a programming language, but I losing my resources by getting redundant info from tables and it also forced me to add redundant code.
So, you need to get customized table-oriented result with columns dynamically generated with SQL ANSI.
You will need a kit with two procedures:
Check if a column has only NULL values or not.
Gather all columns with NO NULL values.
With these two procedures you can check each column of a table strcuture and get an output withtout columns with NULL values.
The first procedure is generic:
set delimiter //
create procedure check_field_null(col varchar(64),
schemaname varchar(255),
tablename varchar(255),
out QN int)
BEGIN
SET #sSQL = concat('SELECT #N := COUNT(*) FROM ', schemaname, '.', tablename , ' WHERE (', col, ' <=> NULL);');
prepare stm from #sSQL;
execute stm;
set QN =#N;
deallocate prepare stm;
END
//
The second procedure means that you have the above mentioned method to identify NOT NULLS columns and put all to search.
set delimiter //
create procedure cur_cs_customer(inout allcols varchar(255), in my_schema_name varchar(64), in my_table_name varchar(64))
BEGIN
DECLARE Count_Null int default 0;
DECLARE initial INT DEFAULT 0;
DECLARE MYCOL char(64);
DECLARE ch_done INT DEFAULT 0;
DECLARE cs_cur1 CURSOR
FOR SELECT C.COLUMN_NAME
FROM information_schema.COLUMNS C
WHERE C.TABLE_SCHEMA = my_schema_name
AND C.TABLE_NAME = my_table_name;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET ch_done = true;
open cs_cur1;
read_cs_cur1:
LOOP
FETCH cs_cur1 INTO MYCOL;
IF (ch_done ) THEN
LEAVE read_cs_cur1;
END IF;
IF NOT isnull(MYCOL) THEN
call check_field_null(MYCOL,
my_schema_name,
my_table_name,
Count_Null);
if Count_Null = 0 then
/* Only it inlcudes fields with not null values */
set initial = initial + 1;
if initial = 1 then
SET allcols = MYCOL;
else
SET allcols = concat( cast(allcols as char(255)), ',', MYCOL);
end if;
end if;
END IF;
END LOOP read_cs_cur1;
close cs_cur1;
select allcols;
END
//
After, you may run as follows:
set delimiter ;
call cur_cs_customer(#my_args, 'schema_name', 'users');
select #my_args;
set #stm = concat('SELECT ', #my_args, ' FROM users;');
PREPARE stmt1 FROM #stm;
execute stmt1;
deallocate prepare stmt1;
Further explanation may be checked in an article here! .
I hope this tip may help you.
I wrote this function:
delimiter //
CREATE FUNCTION randomDefVal(val varchar(30), tableName varchar(30))
returns varchar(30)
BEGIN
SET #query = concat('SELECT ',val,' FROM ',tableName,' ORDER BY rand() LIMIT 1;');
SET #result = NULL;
PREPARE stmt1 FROM #query;
return (EXECUTE stmt1);
END//
But I have an error in the last line:
SQL Error (1336): Dynamic SQL is not allowed in stored function or trigger
Which suggests that I cannot write 'return (EXECUTE stmt1);'
How can I return the value, which will be the result of the 'EXECUTE' statement?
I think what you want is SELECT ... INTO. So you would have something like this within your BEGIN and END (note that I have not tested this code):
BEGIN
DECLARE var_name VARCHAR(30);
SET var_name = '';
SELECT val INTO var_name FROM tableName ORDER BY rand() LIMIT 1;
RETURN var_name;
END
How can I construct a query based on parameters I receive? I want to add to the end of the query.
I tried something like this but it didn't work:
DELIMITER $$
CREATE PROCEDURE `get_users`(IN sortstring TEXT)
BEGIN
PREPARE statement FROM
"SELECT username, password FROM users ?";
SET #param = sortstring;
EXECUTE statement USING #param;
END$$
And I would pass to sortstring something like:
ORDER BY username DESC
Can I do this simpler by using concat or something?
You need to deconstruct the sortstring and check all its parts against a whitelist of allowed terms.
See the following pseudo code. I haven't fully tested it, but lets say it's the idea that counts.
DELIMITER $$
CREATE PROCEDURE `get_users`(IN sortstring TEXT)
BEGIN
//check sortstring against a whitelist of allowed sortstrings
DECLARE sortpart VARCHAR(255);
DECLARE done BOOLEAN DEFAULT false;
DECLARE allok BOOLEAN DEFAULT true;
DECLARE i INTEGER DEFAULT 1;
WHILE ((NOT done) AND allOK) DO
SET sortpart = SUBSTRING_INDEX(sortstring,',',i);
SET i = i + 1;
SET done = (sortpart IS NULL);
IF NOT DONE THEN
SELECT 1 INTO allok WHERE EXISTS
(SELECT 1 FROM whitelist
WHERE allowed_sort_claused = sortpart AND tablename = 'users');
END IF
END WHILE;
IF allOK THEN
PREPARE statement FROM
CONCAT('SELECT username, passhashwithsalt FROM users ',sortstring);
EXECUTE statement;
DEALLOCATE PREPARE statement;
ELSE SELECT 'error' as username, 'error' as passhashwithsalt;
END IF;
END$$
See: How to prevent SQL injection with dynamic tablenames?
The error in your code
You cannot use columnnames or SQL-keywords as parameters. You can only use values as parameters. For that reason your query will never pass prepare.
The ? in SELECT x FROM t1 ? wil just be replaced by SELECT x FROM t1 'ORDER BY field1, field2' Which makes no sense.