SQL Trigger access row's fields dynamically - mysql

Currently trying to have a generic activity log table that stores which table, field, value changed (+ necessary primary key)
DELIMITER $$
CREATE TRIGGER tr_customers_insert_activity_log AFTER INSERT ON `customers`
FOR EACH ROW
BEGIN
DECLARE curr_column CHAR(255);
DECLARE finished INT DEFAULT false;
DECLARE column_name_cursor CURSOR FOR SELECT column_name FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'customers' ORDER BY ordinal_position;
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET finished = 1;
OPEN column_name_cursor;
column_loop: LOOP
IF finished THEN
LEAVE column_loop;
END IF;
FETCH column_name_cursor INTO curr_column;
INSERT INTO activity_log(`cid`, `table`, `field`, `value`, `modified_by`, `modified_at`)
VALUES (NEW.cid, 'customers', curr_column, NEW.#curr_column, NEW.modified_by, NEW.modified_at);
END LOOP column_loop;
CLOSE column_name_cursor;
END$$
DELIMITER ;
The problem I have is in here:
INSERT INTO activity_log(`cid`, `table`, `field`, `value`, `modified_by`, `modified_at`)
VALUES (NEW.cid, 'customers', curr_column, NEW.#curr_column, NEW.modified_by, NEW.modified_at);
Since I am dynamically looping through each field by name I don't know how I can get the NEW.#curr_column value. How can you access a property of the NEW/OLD objects using the value of a variable?
To clarify the syntax error is:
#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 '#curr_column, NEW.modified_by, NEW.modified_at); END LOOP column_loop' at line 17
Thanks!

It's not possible to dynamically address the NEW and OLD values within a TRIGGER.
We can use a CASE expression. But this probably isn't what you were looking for. It's not really "dynamic". We need to statically address each column name we're interested in.
CASE curr_column
WHEN 'cid' THEN NEW.cid
WHEN 'foo' THEN NEW.foo
WHEN 'othercol' THEN NEW.othercol
END
Also problematic is the various datatypes of the columns you might want to store in activity_log table value column... DATE, INTEGER, DECIMAL, ENUM, VARCHAR, ... those are all going to need to be cast to a single datatype of the value column.
Some alternatives to consider:
have the trigger save a copy of the entire row
rather than making the trigger "dynamic", make the creation of the trigger more dynamic... i.e. use a SELECT from information_schema.columns to assist in producing the contents needed in the trigger definition

Related

MySQL PROCEDURE using IF Statement with #Parameter Not Working

Why is the data not being inserted on the table when I execute the procedure, what seems to be lacking with the code?
I'm testing the procedure on phpMyAdmin > myDatabase > Procedures "Routines Tab" and clicking "Execute", prompts with a modal and ask for the values of "#idproc and #nameproc.
I tried with just the INSERT code it works, but when I add the IF condition it doesn't work.
Using XAMPP 8.0.3,
10.4.18-MariaDB
DELIMITER $$
CREATE DEFINER=`root`#`localhost:3307` PROCEDURE `testproc`(IN `idproc` INT, IN `nameproc` VARCHAR(100))
BEGIN
IF #idproc = 0 THEN
INSERT INTO testproc(
id,
name)
VALUES(
#idproc,
#nameproc
);
ELSE
UPDATE testproc
SET
id = #idproc,
name = #nameproc
WHERE id = #idproc;
END IF;
SELECT * FROM testproc;
END$$
DELIMITER ;
You mix local variables (their names have not leading #) and user-defined variables (with single leading #). This is two different variable types, with different scopes and datatype rules. Procedure parameters are local variables too.
So when you use UDV which was not used previously you receive NULL as its value - and your code works incorrectly. Use LV everywhere:
CREATE DEFINER=`root`#`localhost:3307`
PROCEDURE `testproc` (IN `idproc` INT, IN `nameproc` VARCHAR(100))
BEGIN
IF idproc = 0 THEN
INSERT INTO testproc (name) VALUES (nameproc);
ELSE
UPDATE testproc SET name = nameproc WHERE id = idproc;
END IF;
SELECT * FROM testproc;
END
You do not check does specified idproc value exists in the table. If it is specified (not zero) but not exists then your UPDATE won't update anything. Assuming that id is autoincremented primary key of the table I recommend to use
CREATE DEFINER=`root`#`localhost:3307`
PROCEDURE `testproc` (IN `idproc` INT, IN `nameproc` VARCHAR(100))
BEGIN
INSERT INTO testproc (id, name)
VALUES (idproc, nameproc)
ON DUPLICATE KEY
UPDATE name = VALUES(name);
SELECT * FROM testproc;
END
If specified idproc value exists in id column the row will be updated, if not then the new row will be inserted.
Additionally - I recommend you to provide NULL value instead of zero when you want to insert new row with specified nameproc value. NULL always cause autoincremented primary key generation whereas zero needs in specific server option setting.

Create trigger in mysql on insert where it compares fields of added row

I am new with mysql triggers, I have 2 tables in a database, one is called tasks and the other is task_rules.
Once a new task_rule is inserted, I want to compare the field time (which is a time object) to the current time.
if it is greater than the current time, I want to add a new row in tasks and set rid (in tasks) to id of the newly added rule, and the time field in tasks to the time field of the newly added row.
I am getting many syntax errors and i didnt know how to create this trigger.
BEGIN
DECLARE #time TIME
DECLARE #freq VARCHAR(400)
#time = NEW.time
#freq = NEW.frequency
IF (#time > NOW()) AND (#freq == 'daily') THEN
INSERT INTO task_rules ('rid', 'time') VALUES (NEW.id, #time)
END IF
END
Im doing it using phpmyadmin
1) user defined variable (those preceded with #) should not be declared see How to declare a variable in MySQL? 2) to assign a value to a variable you have to use the SET statement 3) every statement must be terminated - if you are using phpmyadmin and the default terminator is set to ; change it and terminate your statements in the trigger with ; see - https://dev.mysql.com/doc/refman/8.0/en/stored-programs-defining.html 4) null safe equals in mysql is not == from memory this should be <=> see https://dev.mysql.com/doc/refman/8.0/en/comparison-operators.html 5) you should probably set delimiters before and after the trigger 6) column names should be escaped with back ticks not single quotes. 7) for each row clause missing before begin statement.
try this
drop trigger if exists t;
delimiter $$
create trigger t after insert on task
for each row
BEGIN
DECLARE vtime TIME;
DECLARE vfreq VARCHAR(400);
set time = NEW.time;
set freq = NEW.frequency;
IF (vtime > NOW()) AND (vfreq <=> 'daily') THEN
INSERT INTO task_rules (`rid`, `time`) VALUES (NEW.id, vtime);
END IF;
END $$
delimiter ;
And do review https://dev.mysql.com/doc/refman/8.0/en/trigger-syntax.html

How to transform/migrate a mysql trigger to a Sql Server Trigger

I did a trigger in mysql to shoot alerts always an input value was less than the set value. But now I need it is done in SQL SERVER.
I would be grateful if someone could help me transform mysql trigger to a SQL Server trigger.
Thanks to all at once.
My trigger is:
DELIMITER $$
create TRIGGER alert
AFTER INSERT ON records
FOR EACH ROW
begin
Set #comp=0;
Set #tempmax=0;
Set #tempmin=0;
select lim_inf_temp into #tempmin from sensores where idSensor=NEW.idSensor;
Set #maxidAlarme=0;
if (CAST(NEW.Temperatura AS UNSIGNED)<#tempmin) then
SELECT MAX(idAlarme) into #maxidAlarme FROM alarmes;
SET #maxidAlarme=#maxidAlarme+1;
INSERT INTO alarmes(idAlarme,descricao_alarme, idRegisto) VALUES (#maxidAlarme,"inserted below the normal temperature",New.idRegisto);
INSERT INTO sensores_tem_alarmes(idSensor,idAlarme,dataAlarme) VALUES (NEW.idSensor,#maxidAlarme,NOW());
set #comp=+1;
end if;
set #id_sensores_em_alerta=1;
SELECT MAX(id_sensores_em_alerta) into #id_sensores_em_alerta FROM sensores_em_alerta;
INSERT INTO sensores_em_alerta(id_sensores_em_alerta, idSensor, idAlarme, data_registo, numerosensoresdisparados) VALUES (id_sensores_em_alerta,NEW.idSensor, #maxidAlarme, NOW(), #comp);
end $$;
DELIMITER ;
I've tried to make the trigger in SQL Server, but as the script is different and I'm getting many difficulties to do the right way.
My attempt that was not going at all well:
CREATE TRIGGER Alert ON registos AFTER INSERT AS
BEGIN
DECLARE #comp decimal= 0
DECLARE #tempmax decimal= 0
DECLARE #tempmin decimal= 0
DECLARE #current_max_idAlarme int = (SELECT MAX(IdAlarme) FROM alarmes)
-- Insert into alarmes from the inserted rows if temperature less than tempmin
INSERT alarmes (IdAlarme, descricao_alarme, idRegisto)
SELECT
ROW_NUMBER() OVER (ORDER BY i.idRegisto) + #current_max_idAlarme,
'temp Error',
i.idRegisto
FROM
inserted AS i
WHERE
i.Temperatura < #tempmin
END
But dont do anything.
Dont create data on table alarmes :S
Does anyone could help me please. I would be eternally grateful.
Many Greetings and thank you all.
First of all, MSSQL doesn't have the option FOR EACH ROW, so it treats multiple inserted rows at once as a set. You will therefore have to insert the values into a table variable.
Unfortunately I do not know much MySQL actually, but I believe this is a starting point?
CREATE TRIGGER ALERT
ON records
AFTER INSERT
AS
BEGIN
DECLARE #comp INT;
DECLARE #tempmax INT;
DECLARE TABLE #tempmin (tempmin INT);
INSERT INTO #tempmin
SELECT s.lim_inf_temp FROM sensores s WHERE s.idSensor IN (inserted.idSensor);
--rest of the code
I'm going to post this code against my better judgement - redesign the tables is better than this hack.
This uses a ROW_number() to virtualise a surrogate identity key for the alarmes table. This is a 'bad plan' (tm).
Also the answer is partial - it doesn't do everything your question asked for -- I hope it gets your further along the road. Use it as a guide for how to interact with the virtual INSERTED table. Good luck
CREATE TRIGGER Alert ON records AFTER INSERT AS
BEGIN
DECLARE #comp INT = 0
DECLARE #tempmax INT = 0
DECLARE #tempmin INT = 0
-- get the max current id.
-- note that this is EXTREMELY unsafe as if two pieces of code are executing
-- at the same time then you *will* end up with key conflicts.
-- you could use SERIALIZABLE.... but better would be to redisn the schema
DECLARE #current_max_idAlarme = (SELECT MAX(IdAlarme) FROM alarmes)
-- Insert into alarmes from the inserted rows if temperature less than tempmin
INSERT alarmes (IdAlarme, descricao_alarme, idRegisto)
SELECT
ROW_NUMBER() OVER (ORDER BY i.idRegisto) + #current_max_idAlarme,
'temp Error',
i.idRegisto
FROM
inserted AS i
WHERE
i.Temperatura < #tempmin
END

IF NOT EXISTS in mysql showing syntax error

I am trying to convert this tsql to mysql but showing error need help
CREATE PROCEDURE FormAdd
#formName varchar(MAX)
AS
IF NOT EXISTS(SELECT * FROM tbl_Form WHERE formName=#formName)
BEGIN
INSERT INTO tbl_Form
(formName)
VALUES
(#formName)
SELECT ##identity
END
ELSE
BEGIN
SELECT '-1'
END
mysql
CREATE PROCEDURE FormAdd
(p_formName varchar(500) )
begin
INSERT INTO tbl_Form (formName)
VALUES (p_formName)
where NOT EXISTS(SELECT * FROM tbl_Form WHERE formName=p_formName) ;
SELECT Last_insert_id() as returnvalue ;
SELECT '-1' ;
end
Your attempt was syntactically invalid because logically, an INSERT statement cannot contain a WHERE clause since it does not act on existing rows.
If the purpose is to insert only if the value for p_formname is not already present, then an appropriate step would be to define a unique index on that column first. Then, construct your procedure to attempt the insert and inspect the ROW_COUNT() value to see if one was inserted and act accordingly, returning -1 if not to adapt your existing T-SQL procedure.
First create the unique index on p_formname:
ALTER TABLE tbl_Form ADD UNIQUE KEY `idx_formName` (`formName`);
Then your procedure should use INSERT INTO...ON DUPLICATE KEY UPDATE to attempt to insert the row. Per the documentation, the value of ROW_COUNT() will be 0 if a new row was not inserted or 1 if it was.
CREATE PROCEDURE FormAdd (p_formName varchar(500))
BEGIN
/* Attempt the insert, overwrite with the same value if necessary */
INSERT INTO tbl_Form (formName) VALUES (p_formName) ON DUPLICATE KEY UPDATE formName = p_formName;
/* Return the LAST_INSERT_ID() for a new row and -1 otherwise */
SELECT
CASE
WHEN ROW_COUNT() = 1 THEN LAST_INSERT_ID()
ELSE -1
END AS returnValue;
END

MySQL: IF / THEN statements in stored procedures

I'm writing a stored procedure that uses multiple IF / THEN statements that also need to execute multiple queries if they evaluate to true. Problem is, I can't seem to find any examples of the appropriate syntax. From the MySQL dev handbook, it seems like I could have multiple queries in the "statement_list," but so far I can't get it to work.
Here's what I'm trying to do:
SET agency =
COALESCE((SELECT org_agency_o_id
FROM orgs_agencies
WHERE org_agency_code = maj_agency_cat)
,(SELECT min(org_id)
FROM orgs
WHERE org_name LIKE CONCAT('U.S.',SUBSTRING(maj_agency_cat,5))))
IF agency IS NULL THEN
-- execute multiple queries
INSERT INTO orgs (org_name
,org_name_length
,org_type
,org_sub_types)
VALUES (CONCAT('U.S. ',SUBSTRING(maj_agency_cat,5))
,LENGTH(CONCAT('U.S. ',SUBSTRING(maj_agency_cat,5)))
,'org','Org,GovernmentEntity,Federal,Agency');
SET agency = LAST_INSERT_ID();
END IF;
The error:
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 'IF agency IS NULL THEN
INSERT INTO orgs (org_name,org_name_length,org_type,' at line 53
Any ideas? I know it has to be something simple, so I would greatly appreciate anybody's input.
You got a few issues as far as I can see:
As David pointed out, each and every statement needs to be terminated by a ;
If you do a SELECT, better make sure it can only select one value by doing a LIMIT 1; If you've got an aggregate function like min() then only one value can come out.
If you writing the procedure using the CREATE PROCEDURE ... syntax, don't forget to set DELIMITER $$ before the CREATE PROCEDURE ... END $$ body and a DELIMITER ; after.
If you have multiple statements inside your IF THEN ... END IF block, it's a good idea to put them inside a BEGIN ... END; block.
If you have a return value, like agency here, why not make it a FUNCTION name (arg1: INTEGER) RETURNS INTEGER instead of a PROCEDURE name (IN arg1 INTEGER, OUT agency INTEGER). The function is much more versatile.
DELIMITER $$
CREATE PROCEDURE name(arg1 INTEGER, arg2 INTEGER, ...)
BEGIN
SELECT SET agency =
COALESCE((SELECT org_agency_o_id
FROM orgs_agencies
WHERE org_agency_code = maj_agency_cat) LIMIT 1,
(SELECT min(org_id) FROM orgs
WHERE org_name LIKE CONCAT('U.S.',SUBSTRING(maj_agency_cat,5))));
IF agency IS NULL THEN BEGIN
-- execute multiple queries
INSERT INTO orgs (org_name
,org_name_length
,org_type
,org_sub_types)
VALUES (CONCAT('U.S. ',SUBSTRING(maj_agency_cat,5))
,LENGTH(CONCAT('U.S. ',SUBSTRING(maj_agency_cat,5)))
,'org','Org,GovernmentEntity,Federal,Agency');
SET agency = LAST_INSERT_ID();
END; END IF;
END $$
DELIMITER ;
No semicolon after your first SET statement.