MySQL Trigger - Storing a SELECT in a variable - mysql

I have a trigger in which I want to have a variable that holds an INT I get from a SELECT, so I can use it in two IF statements instead of calling the SELECT twice. How do you declare/use variables in MySQL triggers?

You can declare local variables in MySQL triggers, with the DECLARE syntax.
Here's an example:
DROP TABLE IF EXISTS foo;
CREATE TABLE FOO (
i SERIAL PRIMARY KEY
);
DELIMITER //
DROP TRIGGER IF EXISTS bar //
CREATE TRIGGER bar AFTER INSERT ON foo
FOR EACH ROW BEGIN
DECLARE x INT;
SET x = NEW.i;
SET #a = x; -- set user variable outside trigger
END//
DELIMITER ;
SET #a = 0;
SELECT #a; -- returns 0
INSERT INTO foo () VALUES ();
SELECT #a; -- returns 1, the value it got during the trigger
When you assign a value to a variable, you must ensure that the query returns only a single value, not a set of rows or a set of columns. For instance, if your query returns a single value in practice, it's okay but as soon as it returns more than one row, you get "ERROR 1242: Subquery returns more than 1 row".
You can use LIMIT or MAX() to make sure that the local variable is set to a single value.
CREATE TRIGGER bar AFTER INSERT ON foo
FOR EACH ROW BEGIN
DECLARE x INT;
SET x = (SELECT age FROM users WHERE name = 'Bill');
-- ERROR 1242 if more than one row with 'Bill'
END//
CREATE TRIGGER bar AFTER INSERT ON foo
FOR EACH ROW BEGIN
DECLARE x INT;
SET x = (SELECT MAX(age) FROM users WHERE name = 'Bill');
-- OK even when more than one row with 'Bill'
END//

CREATE TRIGGER clearcamcdr AFTER INSERT ON `asteriskcdrdb`.`cdr`
FOR EACH ROW
BEGIN
SET #INC = (SELECT sip_inc FROM trunks LIMIT 1);
IF NEW.billsec >1 AND NEW.channel LIKE #INC
AND NEW.dstchannel NOT LIKE ""
THEN
insert into `asteriskcdrdb`.`filtre` (id_appel,date_appel,source,destinataire,duree,sens,commentaire,suivi)
values (NEW.id,NEW.calldate,NEW.src,NEW.dstchannel,NEW.billsec,"entrant","","");
END IF;
END$$
Dont try this # home

`CREATE TRIGGER `category_before_ins_tr` BEFORE INSERT ON `category`
FOR EACH ROW
BEGIN
**SET #tableId= (SELECT id FROM dummy LIMIT 1);**
END;`;

I'm posting this solution because I had a hard time finding what I needed. This post got me close enough (+1 for that thank you), and here is the final solution for rearranging column data before insert if the data matches a test.
Note: this is from a legacy project I inherited where:
The Unique Key is a composite of rridprefix + rrid
Before I took over there was no constraint preventing duplicate unique keys
We needed to combine two tables (one full of duplicates) into the main table which now has the constraint on the composite key (so merging fails because the gaining table won't allow the duplicates from the unclean table)
on duplicate key is less than ideal because the columns are too numerous and may change
Anyway, here is the trigger that puts any duplicate keys into a legacy column while allowing us to store the legacy, bad data (and not trigger the gaining tables composite, unique key).
BEGIN
-- prevent duplicate composite keys when merging in archive to main
SET #EXIST_COMPOSITE_KEY = (SELECT count(*) FROM patientrecords where rridprefix = NEW.rridprefix and rrid = NEW.rrid);
-- if the composite key to be introduced during merge exists, rearrange the data for insert
IF #EXIST_COMPOSITE_KEY > 0
THEN
-- set the incoming column data this way (if composite key exists)
-- the legacy duplicate rrid field will help us keep the bad data
SET NEW.legacyduperrid = NEW.rrid;
-- allow the following block to set the new rrid appropriately
SET NEW.rrid = null;
END IF;
-- legacy code tried set the rrid (race condition), now the db does it
SET NEW.rrid = (
SELECT if(NEW.rrid is null and NEW.legacyduperrid is null, IFNULL(MAX(rrid), 0) + 1, NEW.rrid)
FROM patientrecords
WHERE rridprefix = NEW.rridprefix
);
END

Or you can just include the SELECT statement in the SQL that's invoking the trigger, so its passed in as one of the columns in the trigger row(s). As long as you're certain it will infallibly return only one row (hence one value). (And, of course, it must not return a value that interacts with the logic in the trigger, but that's true in any case.)

As far I think I understood your question
I believe that u can simply declare your variable inside "DECLARE"
and then after the "begin" u can use 'select into " you variable" ' statement.
the code would look like this:
DECLARE
YourVar varchar(50);
begin
select ID into YourVar from table
where ...

Related

How to insert data in a column with a trigger after data is inserted in a row?

I have a program in Laravel where after users register they need a badge number, I want that number to be generated randomly after they register in the database. I should use triggers but I struggle with syntax.
users table
id bigint(20)
name varchar(255)
surname varchar(255)
nr_legitimatie varchar(255)
I want that 'nr_legitimatie' field to be unique.
This is what I tried but with no success
Trigger
CREATE OR REPLACE TRIGGER numar_leg
AFTER INSERT ON users
FOR EACH ROW
DECLARE
legitimatie VARCHAR(191)
BEGIN
legitimatie =('
SELECT FLOOR(RAND() * 99999) AS random_num
FROM numbers_mst
WHERE "random_num" NOT IN (SELECT my_number FROM numbers_mst)
LIMIT 1' );
set `users`.`nr_legitimatie` = legitimatie;
END;
Here's an example of a MySQL BEFORE INSERT trigger that assigns a value to the nr_legitimatie column.
DELIMITER $$
DROP TRIGGER numar_leg$$
CREATE TRIGGER numar_leg
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
DECLARE li_nrn BIGINT DEFAULT NULL;
DECLARE li_cnt BIGINT DEFAULT 1;
WHILE li_cnt > 0 DO
-- generate a new random number
SELECT FLOOR(RAND()*99999) AS nrn INTO li_nrn;
-- check if the new random number is already used
SELECT COUNT(1) INTO li_cnt FROM users u WHERE u.nr_legitimatie = li_nrn;
END WHILE;
SET NEW.nr_legitimatie := li_nrn;
END$$
DELIMITER ;
Note that this does not guarantee that the value assigned to the nr_legitimatie will be unique, because the code in the trigger is subject to a race condition. There is potential for two (or more) simultaneous sessions to each discover the same random number is not yet "unused", and each session will use it. (The check for an existing value precedes the assignment to the column.)
If we want to guarantee uniqueness, we should add a UNIQUE constraint (UNIQUE KEY) on the column in the users table.
We can also use a separate table to track the numbers that are used, with a UNIQUE constraint on the column, we can attempt inserts, and catch the error when an attempt to insert a duplicate is made.
If we introduce a tracking table, e.g.
CREATE TABLE nrn (nrn BIGINT PRIMARY KEY) ;
Then we can avoid the race condition, making the test for existing duplicate and reservation of the new value at the same time. Something like this:
DELIMITER $$
CREATE TRIGGER numar_leg
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
DECLARE li_nrn BIGINT DEFAULT NULL;
DECLARE li_dup BIGINT DEFAULT 1;
DECLARE CONTINUE HANDLER FOR 1062 SET li_dup := 1;
WHILE li_dup > 0 DO
SELECT FLOOR(RAND()*99999) AS nrn INTO li_nrn;
SET li_dup := 0;
INSERT INTO nrn (nrn) VALUES (li_nrn);
END WHILE;
SET NEW.nr_legitimatie := li_nrn;
END$$
DELIMITER ;
The edge case here is the trigger is executed, a new random number is generated and reserved, but the insert into the users table fails for some reason, and we don't issue a ROLLBACK. If we issue a ROLLBACK, then our new random number reservation will also be rolled back (unless nrn is a MyISAM table).

MySQL - Using SELECT for IF statement condition in stored procedure

I want to execute, in a stored procedure, a certain set of statements if, in table my_table there is exactly one row with value value in column column_name. I have tried the following, but I get a syntax error:
IF ((SELECT COUNT(*) FROM my_table WHERE column_name = value) = 1) THEN
BEGIN
END;
END IF;
For context: In my procedure I create a temporary table at some point, where I store a list of values. Then later on in the procedure, I want to check if a given value is present in that temporary table.
I think you might be better to structure it more like this
BEGIN
DECLARE myCOUNT INTEGER;
SELECT COUNT(*)
INTO myCount
FROM my_table
WHERE column_name=value;
IF (myCount = 1) THEN
-- do stuff
END IF;
END;
I'm not sure what you are trying to do, but I'll guess an "upsert" -- update a record if it exists, otherwise insert a new record.
In any case, if you are trying to ensure that name is unique in my_table, then this is not the right approach at all. Instead, declare a unique index/constraint so the database ensures the data integrity:
create unique index unq_my_table_name on my_table(name);
You can then use insert . . . on duplicate key update to modify the records in the database.

How to find out which rows were affected after UPDATE statement inside MySQL procedure?

How to find out affected rows after using UPDATE statements inside procedure in MySQL?(without using an API mysql_affected_rows()). Thanks!
DROP PROCEDURE IF EXISTS users_login;
DELIMITER //
CREATE PROCEDURE users_login(IN _id INT UNSIGNED)
BEGIN
DECLARE _error TINYINT DEFAULT FALSE;
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET _error = TRUE;
UPDATE users SET login = NOW() WHERE id = _id;
IF (_error = TRUE) THEN
SHOW ERRORS;
END IF;
END//
DELIMITER ;
I just want to make some check inside of the procedure.
You don't need your procedure. It just produces unnecessary overhead.
To answer your question. If you want to know, you should do a SELECT first, to check which rows will be affected. If you're using InnoDB (it supports transactions) instead of MyISAM, you can even lock the rows before updating, so that concurrent sessions don't change rows between your select and your update statement. You should do this in a transaction, like this:
START TRANSACTION;
SELECT whatever FROM your_table WHERE foo = 'bar' FOR UPDATE;
UPDATE your_table SET whatever = 'new_value' WHERE foo = 'bar';
COMMIT;
When the transaction is finished with the commit;, the lock is released again.
If you don't want to do a SELECT first, you can use variables to store the primary keys of the affected rows.
(a bit hacky and not tested, but it should work)
SET #affected_ids := NULL;
UPDATE your_table
SET whatever = 'new_value',
primary_key_column = IF(#affected_ids := CONCAT_WS(',', primary_key_column, #affected_ids), primary_key_column, primary_key_column)
WHERE foo = 'bar';
It works like this. The IF() function has the syntax
IF(<boolean expression>, <then>, <else>)
In the <boolean expression> part, we add the current rows primary key to the variable. This is always true. Nonetheless we update the primary key with the primary key, we specify it in both true and false part. The value doesn't change and MySQL is in fact smart enough to not even touch it.
After your UPDATE statement you simply do
SELECT #affected_ids;
and get all rows that were updated.

MySQL: Set to default value on update

Is it possible to set a column to its default value (or any specified value) on update when no value is specifically given in the statement? I was thinking that a trigger might accomplish this. Something like
IF ISNULL(NEW.column) THEN
NEW.column = value
END IF;
didn't work.
MySQL has function called DEFAULT(), which gets the default value from specified column.
UPDATE tbl SET col = DEFAULT(col);
MySQL Reference
UPDATE:
#JanTraenkner As far as I can tell, this is not possible. You can however make sure in your application code, that all columns are mentioned in your update statement and for those that do not have a value your use NULL as value. Then your trigger code is almost right, you just need to change it to
IF (NEW.column IS NULL) THEN
SET NEW.column = value
END IF;
Original answer:
I understood your question like, "set column to default value, if I don't specify the column in an update statement (which updates other columns from that table)".
To check with ISNULL() or col IS NULL doesn't work here, because when you don't specify it in the update statement it simply isn't there. There's nothing to check for.
I wrote this little example script which makes it work like I understood the question.
drop table if exists defvalue;
create table defvalue (id int auto_increment primary key, abc varchar(255) default 'default');
insert into defvalue (id) values (null);
insert into defvalue (id, abc) values (null, 'not_default_value');
insert into defvalue (id, abc) values (null, 'another_not_default_value');
drop trigger if exists t_defval;
delimiter $$
create trigger t_defval before update on defvalue
for each row
begin
set #my_def_value = (select default(abc) from defvalue limit 1);
if (new.abc = old.abc) then
set new.abc = #my_def_value;
end if;
end $$
delimiter ;
select * from defvalue;
update defvalue set id = 99 where id = 1;
select * from defvalue;
update defvalue set id = 98 where id = 2;
select * from defvalue;
I also had to save the default value of the column in a variable first because the function needs to know from which table. Unfortunately one can't specify that as parameter, not even as default(tablename.column).
All in all, please note, that this is rather a proof of concept. I'd recommend to solve this on application layer, not database layer. Having a trigger for this seems a bit dirty for me.

Creating a trigger to update a sort/order column

I have created this trigger to update the seq column. I have to keep track of the order of certain items in the table, but only if the liability_category_id = 1,2. So my ordering is tricky because any item with a liability_category_id = 3 I don't need to track.
In my trigger, I'm querying to find the last entered seq number (using max(seq)), then turning around and updating the new entry with the seq + 1.
DELIMITER $$
USE `analysisdb`$$
DROP TRIGGER /*!50032 IF EXISTS */ `trigger_liability_detail_after_insert`$$
CREATE
/*!50017 DEFINER = 'admin'#'%' */
TRIGGER `trigger_liability_detail_after_insert` AFTER INSERT ON `liability_detail`
FOR EACH ROW BEGIN
DECLARE SortOrder INT;
IF NEW.liability_category_id = 1 OR NEW.liability_category_id = 2 THEN
SET SortOrder = (SELECT MAX(seq) FROM liability_detail WHERE analysis_id = new.analysis_id AND liability_category_id IN (1, 2));
UPDATE liability_detail SET seq = (SortOrder + 1) WHERE id = NEW.id;
END IF;
END;
$$
DELIMITER ;
However, when entering a new item, I get this error: Can't update table 'liability_detail' in stored function/trigger because it is already used by statement which invoked this stored function/trigger.
Is there a better way to control the ordering of these items? My original thought was to simply set the first seq = 1, then seq = 2, etc. The ordering is reset for each new analysis_id though.
I think the workaround is to make this a before trigger and update the record being insert itself prior to insert.
So
CREATE
/*!50017 DEFINER = 'admin'#'%' */
TRIGGER `trigger_liability_detail_after_insert` BEFORE INSERT ON `liability_detail`
FOR EACH ROW BEGIN
DECLARE SortOrder INT;
IF NEW.liability_category_id = 1 OR NEW.liability_category_id = 2 THEN
SET NEW.seq = 1 + IFNULL((SELECT MAX(seq) FROM liability_detail WHERE analysis_id = new.analysis_id AND liability_category_id IN (1, 2)), 0);
END IF;
END;
$$
That was a quick copy/paste, but it should be something along those lines.
That's going to be tricky to handle.
The easy answer is if this could be changed to a BEFORE INSERT FOR EACH ROW trigger,
then you could:
SET NEW.seq = (SortOrder + 1);
to set the value on the row BEFORE it gets inserted into the table. But you can't do that in an AFTER INSERT FOR EACH ROW trigger.
There are some performance and concurrency concerns with using a trigger. (You don't have any guarantee that you won't be generating a "duplicate" value for the seq column when concurrent inserts are running; but that may not be a show stopper issue for you.)
I would prefer the approach of using a simple AUTO_INCREMENT column for the whole table.
The values from that would be "in order" for all the rows, so a query like
... WHERE liability_category_id = 1 ORDER BY seq
would return rows "in the order" those rows were inserted. There would be "gaps" in the sequence number for a given liability_category_id, but the sequence (order) of the inserts would be preserved.
(NOTE: MyISAM has a nifty feature of an AUTO_INCREMENT column, let's it "increment" separately for different values of a leading column in an index. But that only works in the MyISAM engine, it doesn't work in InnoDB.)
Aside from an AUTO_INCREMENT column, I would also consider a TIMESTAMP DEFAULT CURRENT_TIMESTAMP column to record the date/time when the row is inserted.
... WHERE liability_category_id = 1 ORDER BY timestamp_default_current ASC
Both of those approaches are simple column definitions, and do not require any procedural code to be written or maintained.