Create generic trigger in MySQL - mysql

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.

Related

Mysql Dynamic SQL is not allowed in stored function or trigger

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

Trying to convert strings to floats using while and regex, but update function won't change table

I'm trying use mysql to iterate through my database and convert strings to have only numbers and . in order to convert that column into a float from a varchar.
However, when I call the stored proc, it doesn't seem to actually change the datatable. I was hoping somebody could help me figure out what's wrong?
use mydb;
SET GLOBAL log_bin_trust_function_creators = 1;
CREATE TABLE IF NOT EXISTS `docs` (
`id` int(6) unsigned NOT NULL,
`rev` int(3) unsigned NOT NULL,
`content` varchar(200) NOT NULL,
PRIMARY KEY (`id`,`rev`)
) DEFAULT CHARSET=utf8;
-- INSERT INTO `-- docs` (`id`, `rev`, `content`) VALUES
-- ('1', '1', '0.0 '),
-- ('2', '1', '2,765'),
-- ('3', '1', '*'),
-- ('4', '1', '0.7665');
DROP FUNCTION IF EXISTS floatify;
DELIMITER //
CREATE FUNCTION floatify(str VARCHAR(200)) RETURNS VARCHAR(16)
BEGIN
DECLARE i, len SMALLINT DEFAULT 1;
DECLARE ret VARCHAR(16) DEFAULT '';
DECLARE c CHAR(1);
SET len = CHAR_LENGTH( str );
REPEAT
BEGIN
SET c = MID( str, i, 1 );
IF c REGEXP '[0-9|.]' THEN
SET ret=CONCAT(ret,c);
END IF;
SET i = i + 1;
END;
UNTIL i > len END REPEAT;
RETURN ret;
END//
DELIMITER ;
DROP PROCEDURE IF EXISTS tofloat;
DELIMITER //
CREATE PROCEDURE tofloat()
BEGIN
DECLARE currentid INT DEFAULT 1;
DECLARE RowCnt BIGINT DEFAULT 0;
SELECT RowCnt = COUNT(*) FROM docs;
WHILE currentid <= RowCnt DO
UPDATE docs
SET content = floatify(content)
WHERE id = currentid;
SET currentid = currentid + 1 ;
END WHILE;
-- ALTER TABLE docs
-- MODIFY content FLOAT NOT NULL DEFAULT 0 ;
END//
DELIMITER ;
-- select * from docs;
CALL tofloat();
select * from docs;
SET RowCnt = (SELECT COUNT(*) FROM docs); fixed it
You can set a variable to be the result of a subquery within a proc.
Get SQL to do the work:
ALTER TABLE t ADD COLUMN cfloat FLOAT;
UPDATE t SET cfloat = 0 + content;
ALTER TABLE t
CHANGE COLUMN content content_str VARCHAR(200) NOT NULL,
CHANGE COLUMN cfloat content FLOAT NOT NULL;
...check the results
ALTER TABLE t
DROP COLUMN content;

Mysql 5.7 Issue in declare handler for duplicate key

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.

#1442 - Can't update table ... in stored function/trigger because it is already used by statement which invoked this stored function/trigger

I have this situation:
Two table in two databases:
1st: `persone`.`T_Persone` that contain detailed information about workers plus two fields empty by default: `user_id` and `username`;
2nd: `joomla`.`fe48y_users` that contain information of user in the CMS Joomla including `id` and `username`.
I created a procedure that insert a new user in joomla and return user_id and username of new user created.
I also created also 2 triggers (on update and on insert) that call the procedure and set user_id and username on `T_Persone` table.
I also created a trigger on delete of `joomla`.`fe48y_users` that update `persone`.`T_Persone` and set null to `user_id` and `username` fields.
This is the procedure:
CREATE DEFINER=`root`#`localhost` PROCEDURE `persone_users_upd`(IN `in_id` INT(10) UNSIGNED, IN `new_email` VARCHAR(255), IN `old_email` VARCHAR(255), OUT `out_user_id` INT(10) UNSIGNED, OUT `out_username` VARCHAR(255))
BEGIN
DECLARE has_email Boolean;
DECLARE my_user_id INT;
DECLARE my_username VARCHAR(255);
DECLARE my_name VARCHAR(255);
SELECT (CASE new_email WHEN '' THEN FALSE ELSE TRUE END) INTO #has_email;
IF #has_email THEN
SELECT CONCAT(`Nome`,' ',`Cognome`), COALESCE(`CF`,LOWER(SUBSTR(new_email, 1, INSTR(new_email, '#') - 1))) INTO #my_name, #my_username FROM `T_Persone` WHERE `IDPersona` = in_id;
IF new_email = old_email THEN
INSERT INTO `spmsf`.`fe48y_users` (`name`, `username`, `email`, `password`, `block`, `sendEmail`, `registerDate`, `lastvisitDate`, `activation`, `params`, `lastResetTime`, `resetCount`, `otpKey`, `otep`, `requireReset`) VALUES
(#my_name, #my_username, new_email, '<omissis>', 0, 1, NOW(), '0000-00-00 00:00:00', '', '{"admin_style":"","admin_language":"","language":"","editor":"","helpsite":"","timezone":"Europe/Rome"}', '0000-00-00 00:00:00', 0, '', '', 0) ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `email` = VALUES(`email`);
ELSE
UPDATE `spmsf`.`fe48y_users` SET `email` = #my_email WHERE `email` = new_email;
END IF;
SELECT `id`, `username` INTO #my_user_id, #my_username FROM `spmsf`.`fe48y_users` WHERE `email` = new_email;
INSERT IGNORE INTO `spmsf`.`fe48y_user_usergroup_map` (`user_id`,`group_id`) VALUES (#my_user_id, 10);
SET out_user_id=#my_user_id, out_username=#my_username;
END IF;
END
these are the triggers on `T_Persone`:
CREATE TRIGGER `persone_del` AFTER DELETE ON `T_Persone`
FOR EACH ROW BEGIN
IF OLD.`user_id` IS NOT NULL THEN
DELETE FROM `spmsf`.`fe48y_users` WHERE `id` = OLD.`user_id`;
DELETE FROM `spmsf`.`fe48y_user_usergroup_map` WHERE `user_id` = OLD.`user_id`;
END IF;
END
CREATE TRIGGER `persone_ins` BEFORE INSERT ON `T_Persone`
FOR EACH ROW BEGIN
CALL persone_users_upd(NEW.`IDPersona`,COALESCE(NEW.`Email`,NEW.`Email_alt`),COALESCE(NEW.`Email`,NEW.`Email_alt`), #user_id, #username);
SET NEW.`user_id` = #user_id, NEW.`username` = #username;
END
CREATE TRIGGER `persone_upd` BEFORE UPDATE ON `T_Persone`
FOR EACH ROW BEGIN
CALL persone_users_upd(NEW.`IDPersona`,COALESCE(NEW.`Email`,NEW.`Email_alt`),COALESCE(OLD.`Email`,OLD.`Email_alt`), #user_id, #username);
SET NEW.`user_id` = #user_id, NEW.`username` = #username;
END
and this is the trigger on `fe48y_users` delete:
CREATE TRIGGER `users_del` BEFORE DELETE ON `fe48y_users`
FOR EACH ROW BEGIN
DECLARE my_user_id Int;
SELECT COUNT(*) INTO #my_user_id FROM `personale`.`T_Persone` WHERE `user_id` = OLD.`id`;
IF #my_user_id > 0 THEN
UPDATE `personale`.`T_Persone` SET `user_id` = NULL, `username` = NULL WHERE `user_id` = OLD.`id`;
END IF;
END
SO, I have two problems:
1st: When I try to delete a user in `fe48y_users` I have the error
#1442 - Can't update table 'fe48y_users' in stored function/trigger because it is already used by statement which invoked this stored function/trigger.
2nd: When I insert a new person it don't appear in 'fe48y_users' but when I update it than appear.

MySQL INSERT with CALL

I have a stored procedure that splits a string and ends with a select.
I would like to run an insert on the stored procedure like you would do an insert on a select
Something like this
INSERT INTO ....
CALL sp_split...
My split looks like this:
DELIMITER $$
CREATE DEFINER=`root`#`localhost` PROCEDURE `split_with_id`(id INT, input varchar(1000), delim VARCHAR(10))
BEGIN
declare foundPos tinyint unsigned;
declare tmpTxt varchar(1000);
declare delimLen tinyint unsigned;
declare element varchar(1000);
drop temporary table if exists tmpValues;
create temporary table tmpValues
(
`id` int not null default 0,
`values` varchar(1000) not null default ''
) engine = memory;
set delimLen = length(delim);
set tmpTxt = input;
set foundPos = instr(tmpTxt,delim);
while foundPos <> 0 do
set element = substring(tmpTxt, 1, foundPos-1);
set tmpTxt = replace(tmpTxt, concat(element,delim), '');
insert into tmpValues (`id`, `values`) values (id, element);
set foundPos = instr(tmpTxt,delim);
end while;
if tmpTxt <> '' then
insert into tmpValues (`id`, `values`) values (id, tmpTxt);
end if;
select * from tmpValues;
END
Create a wrapper function and have it call the procedure. Then SELECT it normally.
DELIMITER $$
CREATE FUNCTION `f_wrapper_split` (strin VARCHAR(255))
RETURNS VARCHAR(255)
BEGIN
DECLARE r VARCHAR(255);
CALL sp_split(strin);
RETURN r;
END
$$
Of course, if sp_split returns multiple results, you'll need to adapt the function to, perhaps, take an INT input as well and return you that particular result. Then just call it multiple times.
It's not very pretty, but that's the best I can think of offhand.