We are facing an issue in declaring handlers in MYSQL 5.7 stored procedure.
Using the documentation present in https://dev.mysql.com/doc/refman/5.7/en/get-diagnostics.html, I'm trying to create handler in my stored procedure to do the following:
If a duplicate key appears, log it and move on insert good row.
End of the stored proc, get me all the error and success rows inserted.
As you can see below, we don't have primary key for table, however have the unique key constraint.
Input
CALL userTableInsert('{"userid":100, "username":"Hello"}, {"userid":102, "username":"World"}, {"userid":102, "username":"World"}', #got_dups)
Expected Output
100:Hello should get inserted and 102:World should get errored out and status needs to be corrected depicted in the output of the stored procedure.
Getting ERR : 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 'DECLARE CONTINUE HANDLER FOR SQLEXCEPTION ...'
Table: user_table
CREATE TABLE `user_table` (
user_id int(20) NOT NULL,
user_name varchar(255) DEFAULT NULL,
created_by varchar(255) NOT NULL,
created_date date NOT NULL,
UNIQUE KEY `user_ukey` (user_id, user_name)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
===============================================
Stored Procedure
===============================================
DELIMITER $$
DROP PROCEDURE IF EXISTS schema.userTableInsert$$
CREATE PROCEDURE schema.userTableInsert(IN JSONArrayParam MEDIUMTEXT, OUT
got_dups INT)
SQL SECURITY INVOKER
BEGIN
DECLARE code CHAR(5) DEFAULT '00000';
DECLARE msg TEXT;
DECLARE rows INT;
DECLARE result TEXT;
DECLARE rowsJSON MEDIUMTEXT DEFAULT NULL;
SET rowsJSON = TRIM(JSONArrayParam);
SET rowsJSON = TRIM(LEADING '[' FROM rowsJSON);
SET rowsJSON = TRIM(TRAILING ']' FROM rowsJSON);
DROP TEMPORARY TABLE IF EXISTS rowsToUse;
CREATE TEMPORARY TABLE rowsToUse (
user_id INT NOT NULL,
user_name VARCHAR(255) DEFAULT NULL
) ENGINE=MEMORY;
WHILE (rowsJSON != '') DO
BEGIN
DECLARE rowJSON VARCHAR(4096) DEFAULT NULL;
DECLARE temp JSON;
DECLARE userid INT DEFAULT NULL;
DECLARE username VARCHAR(255) DEFAULT NULL;
SET rowJSON = CONCAT(SUBSTRING_INDEX(rowsJSON, '}', 1), '}');
SET temp = JSON_EXTRACT(rowJSON, '$.userid');
SET userid = COALESCE(IF(JSON_TYPE(temp) = 'NULL', NULL, REPLACE(temp, '\"', '')));
SET temp = JSON_EXTRACT(rowJSON, '$.username');
SET username = COALESCE(IF(JSON_TYPE(temp) = 'NULL', NULL, REPLACE(temp, '\"', '')));
INSERT INTO rowsToUse(user_id, user_name)
VALUES(userid, username);
IF LOCATE('{', rowsJSON, 2) > 0 THEN
SET rowsJSON = SUBSTRING(rowsJSON, LOCATE('{', rowsJSON, 2));
ELSE
SET rowsJSON = '';
END IF;
END;
END WHILE;
SET SQL_SAFE_UPDATES=0;
# Getting syntax error here
DECLARE CONTINUE HANDLER FOR 1062
BEGIN
GET DIAGNOSTICS CONDITION 1
code = RETURNED_SQLSTATE, msg = MESSAGE_TEXT;
END;
INSERT INTO user_table(user_id, user_name, created_by , created_date)
SELECT user_id, user_name, 'system', NOW()
FROM rowsToUse;
IF code = '00000' THEN
GET DIAGNOSTICS rows = ROW_COUNT;
SET result = CONCAT('insert succeeded, row count = ',rows);
ELSE
SET result = CONCAT('insert failed, error = ',code,', message = ',msg);
END IF;
DROP TEMPORARY TABLE IF EXISTS rowsToUse;
SET SQL_SAFE_UPDATES=1;
SELECT result;
END
$$
DELIMITER ;
==============================================================
Kindly help me on this.
Related
To not publicly disclose our amount of invoices, we want to add random value between 2 ids.
Instead of [1,2,3] we want something like [69,98,179]
UUID is not an option in that project, unfortunately.
Using Mysql 5.7, 8, or MariaDb get the same results.
Here is the approach is taken:
Consider a simple table invoices as follows:
CREATE TABLE `invoices` (
`id` bigint(20) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=42 DEFAULT CHARSET=utf8mb4;
The function to get random values:
DROP FUNCTION IF EXISTS random_integer;
CREATE FUNCTION random_integer(value_minimum INT, value_maximum INT)
RETURNS INT
LANGUAGE SQL
NOT DETERMINISTIC
RETURN FLOOR(value_minimum + RAND() * (value_maximum - value_minimum + 1));
The function to get the next id:
DROP FUNCTION IF EXISTS next_invoice_id_val;
DELIMITER //
CREATE FUNCTION next_invoice_id_val ()
RETURNS BIGINT(8)
LANGUAGE SQL
NOT DETERMINISTIC
BEGIN
DECLARE lastId BIGINT(8) DEFAULT 1;
DECLARE randId BIGINT(8) DEFAULT 1;
DECLARE newId BIGINT(8) DEFAULT 1;
DECLARE nextId BIGINT(8) DEFAULT 1;
SELECT (SELECT MAX(`id`) FROM `invoices`) INTO lastId;
SELECT (SELECT random_integer(1,10)) INTO randId;
SELECT ( lastId + randId ) INTO nextId;
IF lastId IS NULL
THEN
SET newId = randId;
ELSE
SET newId = nextId;
END IF;
RETURN newId;
END //
DELIMITER ;
SELECT next_invoice_id_val();
and the trigger:
DROP TRIGGER IF EXISTS next_invoice_id_val_trigger;
DELIMITER //
CREATE TRIGGER next_invoice_id_val_trigger
BEFORE INSERT
ON invoices FOR EACH ROW
BEGIN
SET NEW.id = next_invoice_id_val();
END//
DELIMITER ;
That work like a charm, now if we want to generalize the behaviour to all tables.
We need a procedure to execute the query on any specific tables:
DROP PROCEDURE IF EXISTS last_id;
DELIMITER //
CREATE PROCEDURE last_id (IN tableName VARCHAR(50), OUT lastId BIGINT(8))
COMMENT 'Gets the last id value'
LANGUAGE SQL
NOT DETERMINISTIC
READS SQL DATA
BEGIN
SET #s := CONCAT('SELECT MAX(`id`) FROM `',tableName,'`');
PREPARE QUERY FROM #s;
EXECUTE QUERY;
DEALLOCATE PREPARE QUERY;
END //
DELIMITER ;
CALL last_id('invoices', #nextInvoiceId);
SELECT #nextInvoiceId;
The procedure for the next id value:
DROP PROCEDURE IF EXISTS next_id_val;
DELIMITER //
CREATE PROCEDURE next_id_val (IN tableName VARCHAR(50), OUT nextId BIGINT(8))
COMMENT 'Give the Next Id value + a random value'
LANGUAGE SQL
NOT DETERMINISTIC
READS SQL DATA
BEGIN
DECLARE randId BIGINT(8) DEFAULT 1;
SELECT (SELECT random_integer(1,10)) INTO randId;
CALL last_id(tableName, #currentId);
IF #currentId IS NULL
THEN
SET nextId = randId;
ELSE
SELECT ( #currentId + randId ) INTO nextId;
END IF;
END //
DELIMITER ;
CALL next_id_val('invoices', #nextInvoiceId);
SELECT #nextInvoiceId;
and the trigger:
# Call the procedure from a trigger
DROP TRIGGER IF EXISTS next_invoice_id_val_trigger;
DELIMITER //
CREATE TRIGGER next_invoice_id_val_trigger
BEFORE INSERT
ON invoices FOR EACH ROW
BEGIN
CALL next_id_val('invoices', #nextInvoiceId);
SET NEW.id = #nextInvoiceId;
END//
DELIMITER ;
and we get => Dynamic SQL is not allowed in stored function or trigger
I've read that storing in a temporary table might be a workaround, but as all posts have between 5 to 10 years old, I think we might have a better solution for such a straightforward case.
What is the workaround for using dynamic SQL in a stored Procedure
#1336 - Dynamic SQL is not allowed in stored function or trigger
Calling stored procedure that contains dynamic SQL from Trigger
Alternatives to dynamic sql in stored function
I am trying to rename columns of a new table based on the row values of a table.
I am using a cursor to fetch the column values corresponding to the old table name. Then altering the newly created table to rename the columns by using the values fetched by the cursor.
The following table shows the columns to be remapped from the client_label to master_label for the respective client_id.
CREATE TABLE `mapping_table` (
`_id` int(11) NOT NULL,
`master_label` varchar(50) DEFAULT NULL,
`client_label` varchar(50) DEFAULT NULL,
`org_label` varchar(50) DEFAULT NULL,
`client_id` varchar(3) DEFAULT NULL
);
INSERT INTO `mapping_table` (`_id`, `master_label`, `client_label`,
`organization_label`, `client_id`) VALUES
(1, 'CUST-ID', 'client', 'TEST', 'tes'),
(8, 'TRANSACTION-ID', 'trno', 'TEST', 'tes'),
(9, 'TRANSACTION-DATE', 'date', 'TEST', 'tes'),
(14, 'PROD-NAME', 'product', 'TEST', 'rbb'),
(16, 'SALES-FIRSTNAME', 'firstname', 'TEST', 'tes')
;
The Procedure looks like this..
BEGIN
DECLARE old_names varchar(50) default null;
DECLARE new_names varchar(50) default null;
DECLARE num_cols varchar(3) DEFAULT null;
DECLARE i INT(3) DEFAULT null;
DECLARE cur_old_names CURSOR for SELECT client_label from`mapping_table` where client_id = ip_client_id order by _id;
DECLARE cur_new_names CURSOR for SELECT master_label from `mapping_table` where client_id = ip_client_id order by _id;
BEGIN
if ip_client_id = (SELECT DISTINCT(client_id) from `mapping_table` where client_id = ip_client_id) THEN
CREATE TABLE `test01`
SELECT * FROM `ROOT_TABLE`;
select count(client_id) into num_cols from `mapping_table` where client_id=ip_client_id;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- open the cursor
OPEN cur_old_names;
OPEN cur_new_names;
my_loop: LOOP
FETCH NEXT FROM cur_new_names INTO val1;
FETCH NEXT FROM cur_old_names INTO val2;
IF done THEN -- this will be true when we are out of rows to read, so we go to the statement after END LOOP.
LEAVE my_loop;
ELSE
ALTER TABLE `test01 ` change val1 val2 varchar(50);
END IF;
END LOOP;
close cur_old_names;
close cur_new_names;
end if;
END;
END
The test01 table should contain column names from the cursor cur_new_names fetched into val2
While executing, I get the error message,
The following query has failed: "SET #p0='tes'; CALL
`map_column_names`(#p0); "MySQL said: #1054 - Unknown column 'val1' in 'test01_normalized_02'
In Postgresql, trigger can be created by using trigger procedure. This is handy way of creating trigger. Using the same trigger procedure, it is possible to create several triggers and apply it even for several different tables. I am wondering if there is any MySQL equivalent for it. I am inspired by this blog post which creates a generic trigger for database auditing. My plan is to implement the similar approach by using MySQL. But, is it really possible create that kind of generic trigger by MySQL?
After doing some research, I have understood that there is no direct way to create generic trigger in MySQL. Even dynamic SQL like prepare statement, execute statement are not allowed inside trigger in MySQL.
I have found a workaround to generate trigger dynamically. Suppose we have a customer table:
CREATE TABLE customer (
id bigint(20) NOT NULL AUTO_INCREMENT,
created_on datetime DEFAULT NULL,
first_name varchar(100) NOT NULL,
last_name varchar(100) NOT NULL,
PRIMARY KEY (id)
)
A revision info table:
CREATE TABLE REVINFO (
REV int(11) NOT NULL AUTO_INCREMENT,
REVTSTMP bigint(20) DEFAULT NULL,
PRIMARY KEY (REV)
)
An audit table:
CREATE TABLE customer_AUD (
id bigint(20) NOT NULL,
REV int(11) NOT NULL,
REVTYPE tinyint(4) DEFAULT NULL,
created_on datetime DEFAULT NULL,
first_name varchar(100) DEFAULT NULL,
last_name varchar(100) DEFAULT NULL,
PRIMARY KEY (id, REV),
KEY FK_REV (REV)
)
Now we will crate a procedure that will take a table name and generate SQL for create audit related trigger for table.
DROP PROCEDURE IF EXISTS `proc_trigger_generator`;
DELIMITER $$
CREATE PROCEDURE `proc_trigger_generator` (IN tableName VARCHAR(255))
BEGIN
DECLARE triggerSQL TEXT DEFAULT "";
DECLARE cols TEXT DEFAULT "";
DECLARE col_values TEXT DEFAULT "";
DECLARE insert_query TEXT DEFAULT "";
DECLARE colName TEXT DEFAULT "";
DECLARE done INT DEFAULT FALSE;
DECLARE cursorDS CURSOR FOR SELECT column_name FROM information_schema.columns cols
WHERE cols.table_name = CONCAT(tableName, '_AUD')
and (column_name != 'REV' && column_name != 'REVTYPE');
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
SET triggerSQL = 'DELIMITER ;; \n\n';
SET triggerSQL = CONCAT(triggerSQL, 'drop trigger if exists tr_', tableName, '_update_audit;; \n\n');
SET triggerSQL = CONCAT(triggerSQL, 'create trigger tr_', tableName, '_update_audit \n');
SET triggerSQL = CONCAT(triggerSQL, 'after update \n');
SET triggerSQL = CONCAT(triggerSQL, '\t on ', tableName, '\n');
SET triggerSQL = CONCAT(triggerSQL, 'for each row \n');
SET triggerSQL = CONCAT(triggerSQL, 'begin \n');
SET triggerSQL = CONCAT(triggerSQL, '\t DECLARE tmpInt INT; \n');
SET triggerSQL = CONCAT(triggerSQL, '\t SELECT COALESCE(MAX(REV), 0) FROM REVINFO into tmpInt; \n\n');
SET triggerSQL = CONCAT(triggerSQL, '\t INSERT INTO REVINFO (REV, REVTSTMP) VALUES (tmpInt+1, CURRENT_TIMESTAMP()); \n\n');
SET insert_query = CONCAT(insert_query, 'INSERT INTO ', CONCAT(tableName, '_AUD'), ' (');
OPEN cursorDS;
ds_loop: LOOP
FETCH cursorDS INTO colName;
IF done THEN
LEAVE ds_loop;
END IF;
SET cols = CONCAT(cols, colName, ', ');
SET col_values = CONCAT(col_values, 'new.', colName, ', ');
END LOOP;
SET insert_query = CONCAT(insert_query, cols, 'REV, REVTYPE) VALUES \n');
SET insert_query = CONCAT(insert_query, '\t\t(', col_values, 'tmpInt+1, 1', ');');
CLOSE cursorDS;
SET triggerSQL = CONCAT(triggerSQL, '\t ',insert_query, ' \n\n');
SET triggerSQL = CONCAT(triggerSQL, 'end;; \n\n');
SET triggerSQL = CONCAT(triggerSQL, 'DELIMITER ; \n\n');
SELECT triggerSQL;
END $$
DELIMITER ;
call proc_trigger_generator('customer');
Calling the procedure by using customer table name generates SQL for the desired trigger:
DELIMITER ;;
drop trigger if exists tr_customer_update_audit;;
create trigger tr_customer_update_audit
after update
on customer
for each row
begin
DECLARE tmpInt INT;
SELECT COALESCE(MAX(REV), 0) FROM REVINFO into tmpInt;
INSERT INTO REVINFO (REV, REVTSTMP) VALUES (tmpInt+1, CURRENT_TIMESTAMP());
INSERT INTO customer_AUD (id, created_on, first_name, last_name, REV, REVTYPE) VALUES
(new.id, new.created_on, new.first_name, new.last_name, tmpInt+1, 1);
end;;
DELIMITER ;
The above trigger should do auditing tasks for customer table.
The trigger generator procedure can be applied to any other table now that we wish to apply auditing related tasks.
I am constantly getting a Error code 1062: Duplicate Entry.
The first row insert, but then it fails on the same ID.
So everytime I hit execute it will increment: 1466, 1467, 1468, 1469.
And each time there is the same record entered, so I am assuming the autoincrement is only working for the first iteration.
Table:
'entity':
CREATE TABLE `entity` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`reg_num` varchar(45) NOT NULL,
`enterprise_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1474 DEFAULT CHARSET=latin1 COMMENT=\'Comment'
Stored procedure:
DELIMITER $$
CREATE DEFINER=`root`#`localhost` PROCEDURE `tp_to_entityPROC`()
DETERMINISTIC
COMMENT 'stored'
BEGIN
DECLARE done BOOLEAN DEFAULT 0;
DECLARE Tid INT;
DECLARE Tt_name TEXT;
DECLARE allt CURSOR FOR
SELECT training_provider_id, training_provider_name
FROM training_providers;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done=1;
OPEN allt;
read_loop: LOOP
IF done THEN
LEAVE read_loop;
END IF;
FETCH allt INTO Tid, Tt_name;
SET #id = 0;
SET #t_name = 0;
SET #id = Tid;
SET #t_name = Tt_name;
SET #empty = '';
if (#id != 0) THEN
INSERT INTO entity (name)
VALUES (#t_name);
SET #my_id = LAST_INSERT_ID();
IF #my_id != 0 THEN
UPDATE training_awarded_providers
SET training_awarded_provider_id = #my_id
WHERE training_awarded_provider_id = #id;
END IF;
END IF;
END LOOP;
CLOSE allt;
END
Not sure about the exact error of duplicate entry but your posted code is not going to work.
Your Table schema
CREATE TABLE `entity` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`reg_num` varchar(45) NOT NULL <-- Here it's non null column
In your store procedure you are trying to insert null to reg_num column which will never succeed
if (#id != 0) THEN
INSERT INTO entity (name)
VALUES (#t_name);
I am trying to call a stored procedure, but I am getting : Error Code: 1175 You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column
Here is the table:
CREATE TABLE `SystemSetting` (
`SettingName` varchar(45) NOT NULL,
`SettingValue` varchar(45) NOT NULL,
`Deleted` bit(1) NOT NULL DEFAULT b'0',
PRIMARY KEY (`SettingName`),
KEY `PK_SystemSetting` (`SettingName`),
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
and the procedure:
DELIMITER $$
CREATE PROCEDURE `SystemSettingCommit` (IN p_SettingName varchar(45), IN p_SettingValue varchar(45), OUT p_Status varchar(55))
BEGIN
SET p_Status = '00000';
IF (p_SettingValue IS NULL) OR (RTRIM(LTRIM(p_SettingValue)) = '') THEN
SET p_Status = `StatusConcat`(p_Status, '10016');
END IF;
IF (p_SettingName IS NULL) OR (RTRIM(LTRIM(p_SettingName)) = '') THEN
SET p_Status = `StatusConcat`(p_Status, '10017');
END IF;
IF (p_Status = '00000') THEN
IF ((SELECT COUNT(`SettingName`) FROM `SystemSetting` WHERE (`SettingName` = p_SettingName)) > 0) THEN
UPDATE `SystemSetting` SET `SettingValue` = p_SettingValue WHERE (`SettingName` = p_SettingName);
ELSE
INSERT INTO `SystemSetting` (`SettingName`, `SettingValue`) VALUES (p_SettingName, p_SettingValue);
END IF;
END IF;
END$$
DELIMITER;
and this is how I call it:
CALL `SystemSettingCommit` ('Setting1', 'Value1', #Status);
Try this:
SET SQL_SAFE_UPDATES=0;
Or follow this in workbench:
Go to Edit --> Preferences
Click "SQL Queries" tab and uncheck "Safe Updates" check box
Query --> Reconnect to Server
Now execute your sql query