Prepared Statement Failure
One of my students asked for some help on developing a MySQL stored procedure that would conditionally drop a foreign key constraint. I put together a quick example, which compiled fine but failed at run time. With some investigation it appears that either the MySQL 5.5 Documentation in Section 12.6 is incorrect or you can’t ALTER TABLE
inside a MySQL Prepared Statement with placeholders.
A subsequent test showed me that you couldn’t use ALTER TABLE
statement in a prepared statement outside of a stored procedure. I logged Bug #59604 with my other test case, and put the test case there. They closed the bug and validated what I suspected and clarified what I missed, you can’t use placeholders in prepared DDL statements.
The following is the test code example that failed:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | DROP PROCEDURE IF EXISTS dropForeignKey; DELIMITER $$ CREATE PROCEDURE dropForeignKey ( pv_database VARCHAR(64) , pv_table VARCHAR(64) , pv_constraint VARCHAR(64)) BEGIN /* Declare a local variable for the SQL statement. */ DECLARE stmt VARCHAR(1024); /* Set a session variable with two parameter markers. */ SET @SQL := 'ALTER TABLE ? DROP FOREIGN KEY ?'; /* Assign the formal parameters to session variables because prepared statements require them. */ SET @sv_table := pv_table; SET @sv_constraint := pv_constraint; /* Check if the constraint exists. */ IF EXISTS (SELECT NULL FROM information_schema.referential_constraints WHERE constraint_schema = pv_database AND TABLE_NAME = pv_table AND constraint_name = pv_constraint) THEN /* Dynamically allocated and run statement. */ PREPARE stmt FROM @SQL; EXECUTE stmt USING @sv_table, @sv_constraint; DEALLOCATE PREPARE stmt; END IF; END; $$ DELIMITER ; |
Calling this with the following syntax:
CALL dropForeignKey(DATABASE(),'telephone','telephone_fk4'); |
It raised the following error message.
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 '? DROP FOREIGN KEY ?' at line 1 |
Based on the comment from Bug #59604, I concluded that the ALTER TABLE
statement doesn’t support using session variables. However, the CONCAT()
function solves the problem. Given this is a DDL command, and any extraneous quoting would simply fail parsing rules because of the CONCAT
function, SQL injection doesn’t appear a threat.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | DROP PROCEDURE IF EXISTS dropForeignKey; DELIMITER $$ CREATE PROCEDURE dropForeignKey ( pv_database VARCHAR(64) , pv_table VARCHAR(64) , pv_constraint VARCHAR(64)) BEGIN /* Declare a local variable for the SQL statement. */ DECLARE stmt VARCHAR(1024); /* Set a session variable with two parameter markers. */ SET @SQL := CONCAT('ALTER TABLE ',pv_table,' DROP FOREIGN KEY ',pv_constraint); /* Check if the constraint exists. */ IF EXISTS (SELECT NULL FROM information_schema.referential_constraints WHERE constraint_schema = pv_database AND TABLE_NAME = pv_table AND constraint_name = pv_constraint) THEN /* Dynamically allocated and run statement. */ PREPARE stmt FROM @SQL; EXECUTE stmt; DEALLOCATE PREPARE stmt; END IF; END; $$ DELIMITER ; |
A more useful approach would be to drop all foreign keys that reference a table. Here’s how you would accomplish that.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | -- Conditionally drop the procedure. DROP PROCEDURE IF EXISTS dropForeignKeys; -- Change delimiter to create procedure. DELIMITER $$ -- Create procedure. CREATE PROCEDURE dropForeignKeys ( pv_database VARCHAR(64) , pv_referenced_table VARCHAR(64)) BEGIN /* Declare local statement variables. */ DECLARE lv_stmt VARCHAR(1024); /* Declare local cursor variables. */ DECLARE lv_table_name VARCHAR(64); DECLARE lv_constraint_name VARCHAR(64); /* Declare control variable for handler. */ DECLARE fetched INT DEFAULT 0; /* Declare local cursor. */ DECLARE foreign_key_cursor CURSOR FOR SELECT rc.table_name , rc.constraint_name FROM information_schema.referential_constraints rc WHERE constraint_schema = pv_database AND referenced_table_name = pv_referenced_table ORDER BY rc.table_name , rc.constraint_name; /* Declare a not found record handler to close a cursor loop. */ DECLARE CONTINUE HANDLER FOR NOT FOUND SET fetched = 1; /* Open a local cursor. */ OPEN foreign_key_cursor; cursor_foreign_key: LOOP FETCH foreign_key_cursor INTO lv_table_name , lv_constraint_name; /* Place the catch handler for no more rows found immediately after the fetch operation. */ IF fetched = 1 THEN LEAVE cursor_foreign_key; END IF; /* Set a SQL statement by using concatenation. */ SET @SQL := CONCAT('ALTER TABLE ',lv_table_name,' DROP FOREIGN KEY ',lv_constraint_name); /* Prepare, run, and deallocate statement. */ PREPARE lv_stmt FROM @SQL; EXECUTE lv_stmt; DEALLOCATE PREPARE lv_stmt; END LOOP cursor_foreign_key; CLOSE foreign_key_cursor; END; $$ -- Reset delimiter to run SQL statements. DELIMITER ; |
You would test it with this call:
CALL dropForeignKeys(DATABASE(),'system_user'); |
As always, I hope this helps somebody.