MySQL Stored Procedure fails with ERROR 1064 (42000) - mysql

I have written a MySQL stored procedure that will add a new partition to an existing table:
DELIMITER //
DROP PROCEDURE IF EXISTS cr_par //
CREATE PROCEDURE cr_par (
IN p_table VARCHAR(256),
IN p_date DATE
) BEGIN
DECLARE stmt VARCHAR(1024);
DECLARE ddl VARCHAR(512);
DECLARE par_name VARCHAR(20) DEFAULT '';
DECLARE par_no INT DEFAULT 0;
DECLARE lt_value INT DEFAULT 0;
SET par_no = TO_DAYS(p_date) + 1;
SET par_name = CONCAT('p', par_no);
SET lt_value = par_no + 1;
SET ddl = CONCAT('ALTER TABLE ', p_table, ' ADD PARTITION (PARTITION ', par_name, ' VALUES LESS THAN (', lt_value, '))');
PREPARE stmt FROM #ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SELECT ddl;
END //
DELIMITER ;
When I run the stored procedure I get this error:
mysql> CALL cr_par('test', '2021-09-13');
ERROR 1064 (42000): 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 'NULL' at line 1
If I comment out the PREPARE, EXECUTE and DEALLOCATE statements and re-run the stored procedure I get this, which is a valid DDL statement:
mysql> CALL cr_par('test', '2021-09-13');
+------------------------------------------------------------------------------+
| ddl |
+------------------------------------------------------------------------------+
| ALTER TABLE test ADD PARTITION (PARTITION p738412 VALUES LESS THAN (738413)) |
+------------------------------------------------------------------------------+
1 row in set (0.01 sec)
I've also tried these variations and all return the same error:
SET ddl = 'ALTER TABLE test ADD PARTITION (PARTITION p738412 VALUES LESS THAN (738413));';
PREPARE stmt FROM #ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
and this...
SET ddl = CONCAT('ALTER TABLE ? ADD PARTITION (PARTITION ? VALUES LESS THAN (?))');
PREPARE stmt FROM #ddl;
EXECUTE stmt USING #p_table, #par_no, #lt_value;
DEALLOCATE PREPARE stmt;
I am using this version of MySQL:
Server version: 8.0.25-15 Percona Server (GPL), Release 15, Revision a558ec2
Does anyone have any ideas what could be causing this, I must be missing something simple?

SET ddl = CONCAT('ALTER TABLE ', p_table, ' ADD PARTITION (PARTITION ', par_name, ' VALUES LESS THAN (', lt_value, '))');
PREPARE stmt FROM #ddl;
Please be aware that ddl and #ddl are two different variables.
The variables you declare with the local variable DECLARE statement have a scope within the body of one stored routine. They are never spelled with a # sigil.
The user-defined variables with the # sigil have a scope of a MySQL session. You don't need to declare these kinds of variables. Just setting the variable to a value implicitly creates the variable.
You cannot SET ddl = ... and expect that string to be read from the #ddl variable. Nor vice-versa.
The PREPARE statement only supports preparing an SQL from a user-defined variable. Which means you must set the #ddl variable to your SQL statement:
SET #ddl = CONCAT('ALTER TABLE ', p_table, ' ADD PARTITION (PARTITION ', par_name, ' VALUES LESS THAN (', lt_value, '))');
Then you don't need to DECLARE ddl at all, since there's no use for that variable.

Related

Execute sql using a stored procedure in mysql

I have the following stored procedure. The idea is to get a list of databases and execute an sql statement.
DELIMITER $$
CREATE PROCEDURE updateMySQL (
IN theSQL varchar(4000)
)
BEGIN
DECLARE finished INTEGER DEFAULT 0;
DECLARE theDatabases varchar(100) DEFAULT "";
-- declare cursor for employee email
DEClARE curDatabase
CURSOR FOR
SELECT schema_name FROM information_schema.schemata where SCHEMA_NAME = 'mydb' order by 1;
-- declare NOT FOUND handler
DECLARE CONTINUE HANDLER
FOR NOT FOUND SET finished = 1;
OPEN curDatabase;
getDatabase: LOOP
FETCH curDatabase INTO theDatabases;
IF finished = 1 THEN
LEAVE getDatabase;
END IF;
-- build email list
-- SET emailList = CONCAT(theDatabases,";",emailList);
SET #sql:=CONCAT('USE ',#curDatabase);
PREPARE dynamic_statement FROM #SQL;
EXECUTE dynamic_statement;
PREPARE dynamic_statement FROM #theSQL;
EXECUTE dynamic_statement;
END LOOP getDatabase;
CLOSE curDatabase;
END$$
DELIMITER ;
I am attempting to execute the stored procedure like this,
SET #theSQL = 'ALTER VIEW `Reports` AS
SELECT DISTINCT
`tableA`.`Id` AS `Id`,
`tableA`.`letterId` AS `letterId`
FROM
`mytable` `tableA`
ORDER BY 1';
call updateMySQL(#theSQL);
EDIT There was an error on executing the procedure,
Error Code: 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 'NULL' at line 1
I am using mysql 8.0.17
Investigate carefully MySQL 8.0 Reference Manual / SQL Statements / Prepared Statements, the section "SQL Syntax Allowed in Prepared Statements".
This section claims FULL list of statements which are allowed in Prepared statements. ALTER VIEW is NOT listed. So it is NOT allowed.
Use DROP VIEW and CREATE VIEW instead.
Always receive and investigate all error messages.
You should change this part
SET #sql:=CONCAT('USE ',#curDatabase);
PREPARE dynamic_statement FROM #SQL;
EXECUTE dynamic_statement;
PREPARE dynamic_statement FROM #theSQL;
EXECUTE dynamic_statement;
to this:
SET #sql:=CONCAT('USE ',#curDatabase);
PREPARE dynamic_statement FROM #SQL;
EXECUTE dynamic_statement;
DEALLOCATE PREPARE dynamic_statement; /* don't forget to deallocate */
/* there's a difference between the variables #theSQL and theSQL (your parameter) */
/* IIRC prepare statements need user defined variables or a syntax error occurs. Therefore I simply assign the parameter to a user-defined variable */
SET #theSQL = theSQL;
PREPARE dynamic_statement FROM #theSQL;
EXECUTE dynamic_statement;
DEALLOCATE PREPARE dynamic_statement;
Read more about user-defined variables here: https://dev.mysql.com/doc/refman/8.0/en/user-variables.html
Here the differences are explained: https://stackoverflow.com/a/1010042/447489
When you don't initialize them, their content is just NULL. Since there's a difference between user-defined variables and local variables (and also your parameter variable), your current solution did nothing.

Dynamic table name variable in stored procedure

I want to create a function that will create a unique random id. The parameters will simply be min (the minimum number), max (the maximum number), and tablename (the name of the table to check to see if the id produced by the rand() function already exists).
I have discovered through other posts that you can't pass table names into functions, because functions can't execute dynamic SQL, but you can pass them into stored procedures. I have found numerous examples on StackOverflow of how to pass table names into stored procedures, and they all boil down to using prepared statements.
I have created a stored procedure as shown below:
DELIMITER $$
CREATE DEFINER=`user`#`localhost` PROCEDURE `rand_id`(IN `min` INT, IN `max` INT, IN `tablename` VARCHAR(20) CHARSET utf8, OUT `uid` INT)
BEGIN
DECLARE count_id int;
SET count_id = 1;
SET #s = CONCAT('COUNT(`id`) INTO count_id FROM `', tablename, '` WHERE `id` = ', uid);
WHILE count_id > 0 DO
SET uid = FLOOR(rand() * max + min);
PREPARE stmt from #s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END WHILE;
END$$
DELIMITER ;
Whenever I run the following code:
CALL rand_id(1000000000, 9999999999, 'test', #id);
SELECT #id;
I get this 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 'NULL' at line 1
I'm at a loss for what's wrong. I saw somewhere that you can't use user variables inside a stored procedure, but that seems to be incorrect because there are a lot of examples on StackOverflow where the correct solutions do just that.
Sorry for my low level of MySQL understanding. I'm sure my code is fraught with syntax errors and poor design. I appreciate any help I can get. I researched this for quite a while and tried many things but to no avail. The above portion of code is the closest I've been able to get, and yields the least errors, but it's still not working.
Thank you.
EDIT: As per the second example in #Barmar's answer, I changed my code to look like this:
BEGIN
DECLARE count_id int;
SET count_id = 1;
SET #s = CONCAT('SELECT COUNT(`id`) INTO count_id FROM `', tablename, '` WHERE `id` = ?');
PREPARE stmt from #s;
WHILE count_id > 0 DO
SET #uid = FLOOR(rand() * max + min);
EXECUTE stmt USING #uid;
END WHILE;
DEALLOCATE PREPARE stmt;
SET uid = #uid;
END
It seems to have fixed my initial problem but now I get this error:
#1327 - Undeclared variable: count_id
EDIT: Here is my code changed to fit #slaakso's answer, and add in what #Barmar said about using #count_id:
DELIMITER $$
CREATE DEFINER=`mjrinker`#`localhost` PROCEDURE `rand_id`(IN `min` BIGINT, IN `max` BIGINT, IN `tablename` VARCHAR(128) CHARSET utf8, OUT `uid` BIGINT)
BEGIN
SET #count_id = 1;
SET uid = 0;
SET #s = CONCAT('SELECT COUNT(`id`) INTO #count_id FROM `', tablename, '` WHERE `id` = ?');
PREPARE stmt from #s;
WHILE #count_id > 0 DO
SET #uid = FLOOR(rand() * max + min);
EXECUTE stmt USING #uid;
END WHILE;
DEALLOCATE PREPARE stmt;
SET uid = #uid;
END$$
DELIMITER ;
You need to assign #s after you assign the uid variable.
You're also missing the SELECT keyword in your query.
SET #count_id = 1
WHILE #count_id > 0 DO
SET uid = FLOOR(rand() * max + min);
SET #s = CONCAT('SELECT COUNT(`id`) INTO #count_id FROM `', tablename, '` WHERE `id` = ', uid);
PREPARE stmt from #s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END WHILE;
But you should actually just prepare the statement once, using a placeholder, which you fill in when using EXECUTE.
SET #count_id = 1
SET #s = CONCAT('SELECT COUNT(`id`) INTO #count_id FROM `', tablename, '` WHERE `id` = ?');
PREPARE stmt from #s;
WHILE #count_id > 0 DO
SET #uid = FLOOR(rand() * max + min);
EXECUTE stmt USING #uid;
END WHILE;
DEALLOCATE PREPARE stmt;
SET uid = #uid;
Note that the parameters to EXECUTE have to be user variables, that's why I changed uid to #uid there. Then we set the output parameter at the end of the loop.
You also need to use a user variable for INTO #count_id.
First of all it is highly unusual to use random numbers as ID's for tables. Mabye you should consider using AUTO_INCREMENT columns.
If you really want to use random numbers, couple of fixes for the code:
You should use value for uid for the first time you run the query
(without it it will be NULL, therefore the error).
You are missing SELECT in your dynamic query
The "INTO count_id" syntax will not work as count_id is not visible inside the dynamic SQL (use #var variable instead)
Your min and max values are declared as INT's, but your passed parameters exceed the INT range (-2147483648 - 2147483647)

Why doesn't my mysql code work

I wrote this code and when I execute it it says I have a problem with mysql syntax near the update statement
set #s1 = (select if ((select count(*) from information_schema.columns where table_name='foo' and column_name='bar_id') > 0,
'select 1',
'alter table foo add column bar_id bigint; update foo set bar_id = baz_id;'));
prepare stmt from #s1;
execute stmt;
deallocate prepare stmt;
If I change my code to
set #s1 = (select if ((select count(*) from information_schema.columns where table_name='foo' and column_name='bar_id') > 0,
'select 1',
'alter table foo add column bar_id bigint;'));
prepare stmt from #s1;
execute stmt;
deallocate prepare stmt;
update foo set bar_id = baz_id;
then it works. but I want the update statement inside the if condition.
I cannot make this into a SP.
Error:
ERROR 1064 (42000): 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 'update foo set bar_id = baz_id' at line 1
In your first code block, you attempt to prepare a string that contains two SQL statements. Unfortunately, MySQL prepare / execute cannot have multiple statements.
If you can't use SP, I think I'd suggest doing this like so:
set #s1 = (select if ((select count(*) from information_schema.columns where table_name='foo' and column_name='bar_id') > 0,
'select 1',
concat('alter table foo add column bar_id bigint default ', baz_id)));
prepare stmt from #s1;
execute stmt;
deallocate prepare stmt;
alter table foo alter column bar_id drop default;
But, honestly, I'd suggest you minimize DDL changes as those can have unpredictable run-time behavior. In this case, that means adding the foo.bar_id out-of-band and just perform an update as needed.
The problem is that MySQL's prepared statements do not support multi-statements.
If you want to script the database structure updates, easiest way is to use a procedure without dynamic SQL (you might want to check the table_schema as well when you are doing the changes).
create procedure sp_fix_structure()
begin
declare v_cnt int;
select count(*) into v_cnt
from information_schema.columns
where table_schema=database() and table_name='foo' and column_name='bar_id';
if (v_cnt=0) then
alter table foo add column bar_id bigint;
update foo set bar_id = baz_id;
end if;
end

MySQL 'Group' syntax error when no Group statement?

I'm trying to run the below script against MySQL server but I get the error:
ERROR 1064 (42000) at line 28: 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 'Group' at line 1
Here's the sql script:
/* Remove old indexes */
USE production;
DROP PROCEDURE IF EXISTS DropIndexes;
DELIMITER //
CREATE PROCEDURE DropIndexes()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE tbl, idx VARCHAR(1000);
DECLARE index_cursor CURSOR FOR SELECT TABLE_NAME, INDEX_NAME FROM information_schema.statistics WHERE table_schema = 'production' AND INDEX_NAME LIKE 'ix_%';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN index_cursor;
read_loop: LOOP
FETCH index_cursor INTO tbl, idx;
IF done THEN
LEAVE read_loop;
END IF;
SET #s = CONCAT('DROP INDEX ',idx,' ON ',tbl);
PREPARE stmt1 FROM #s;
EXECUTE stmt1;
DEALLOCATE PREPARE stmt1;
END LOOP;
CLOSE index_cursor;
END//
DELIMITER ;
CALL DropIndexes();
DROP PROCEDURE IF EXISTS DropIndexes;
Given the error makes no sense with no 'Group' statement I have no idea what the problem is.
Maybe you have an index or table named group. You need to escape reserved words like group with backticks. Try
SET #s = CONCAT('DROP INDEX `',idx,'` ON `',tbl,'`');
It must be your Table name selected from the statistics table. To get it accepted enclose it in back-ticks. And though not required on your index name ( because it starts with ix_ ), better you practice using the back-ticks.
SET #s = CONCAT( 'DROP INDEX `' , idx, '` ON `', tbl, '`' );

MySQL Stored Procedure with a hyphen in value

I have a stored procedure that works, but when I pass a value with a hyphen in it, it errors.
I call my procedure with a value like call create('server-v01',555); and I get the following error:
ERROR 1064 (42000): 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 '-v01' at line 1
My procedure is as follows:
DELIMITER $$
CREATE PROCEDURE ct(tname varchar(20), sessionsnum INT(11))
BEGIN
DECLARE maxnum INT;
SET #s = CONCAT('INSERT INTO sessions_poll (server_name,sessions_num) VALUES(''',tname,''',',sessionsnum,')');
PREPARE stm FROM #s;
EXECUTE stm;
SET #s = CONCAT('DROP TABLE IF EXISTS ', tname);
PREPARE stm FROM #s;
EXECUTE stm;
SET #s = CONCAT('CREATE TABLE ', tname, ' (num INT, max INT)');
PREPARE stm FROM #s;
EXECUTE stm;
SELECT #maxnum:=max(sessions_num) INTO maxnum FROM sessions_poll WHERE server_name=tname AND DATE(ts)=CURDATE();
SET #s = CONCAT('INSERT INTO ', tname, ' (num,max) VALUES (', sessionsnum,',',maxnum,')');
PREPARE stm FROM #s;
EXECUTE stm;
END $$
DELIMITER ;
My question is, how can I handle a variable with a hyphen in it?
Your question is not how to handle variable with a dash, but how to handle a table with a dash. Your procedure tries to create a table with a name specified in tname. To create (or drop) a table like this you need to quote it with backticks.
DROP TABLE IF EXISTS `server-01`;
In particular you need to
SET #s = CONCAT('DROP TABLE IF EXISTS `', tname, '`');
and the same for other instances.
Whether this is what you really want to do is a question, though ;-)