MySQL Before Delete trigger to avoid deleting multiple rows - mysql

I am trying to avoid deletion of more than 1 row at a time in MySQL by using a BEFORE DELETE trigger.
The sample table and trigger are as below.
Table test:
DROP TABLE IF EXISTS `test`;
CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) NOT NULL,
`b` int(11) NOT NULL,
PRIMARY KEY (`id`));
INSERT INTO `test` (`id`, `a`, `b`)
VALUES (1, 1, 2);
INSERT INTO `test` (`id`, `a`, `b`)
VALUES (2, 3, 4);
Trigger:
DELIMITER //
DROP TRIGGER IF EXISTS prevent_multiple_deletion;
CREATE TRIGGER prevent_multiple_deletion
BEFORE DELETE ON test
FOR EACH STATEMENT
BEGIN
IF(ROW_COUNT()>=2) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Cannot delete more than one order per time!';
END IF;
END //
DELIMITER ;
This is still allowing multiple rows deletion. Even if I change the IF to >= 1, still allows the operation.
I my idea is to avoid operations such as:
DELETE FROM `test` WHERE `id`< 5;
Can you help me? I know that the current version of MySQL doesn't allow FOR EACH STATEMENT triggers.
Thank you!

Firstly, getting some syntax error(s) out of our way, from your original attempt:
Instead of FOR EACH STATEMENT, it should be FOR EACH ROW.
Since you have already defined the Delimiter to //; you need to use // (instead of ;) in the DROP TRIGGER IF EXISTS .. statement.
Row_Count() will have 0 value in a Before Delete Trigger, as no rows have been updated yet. So this approach will not work.
Now, the trick here is to use Session-level Accessible (and Persistent) user-defined variables. We can define a variable, let's say #rows_being_deleted, and later check whether it is already defined or not.
For Each Row runs the same set of statements for every row being deleted. So, we will just check whether the session variable already exists or not. If it does not, we can define it. So basically, for the first row (being deleted), it will get defined, which will persist as long as the session is there.
Now if there are more rows to be deleted, Trigger would be running the same set of statements for the remaining rows. In the second row, the previously defined variable would be found now, and we can simply throw an exception now.
Note that there is a chance that within the same session, multiple delete statements may get triggered. So before throwing exception, we need to set the #rows_being_deleted value back to null.
Following will work:
DELIMITER //
DROP TRIGGER IF EXISTS prevent_multiple_deletion //
CREATE TRIGGER prevent_multiple_deletion
BEFORE DELETE ON `test`
FOR EACH ROW
BEGIN
-- check if the variable is already defined or not
IF( #rows_being_deleted IS NULL ) THEN
SET #rows_being_deleted = 1; -- set its value
ELSE -- it already exists and we are in next "row"
-- just for testing to check the row count
-- SET #rows_being_deleted = #rows_being_deleted + 1;
-- We have to reset it to null, as within same session
-- another delete statement may be triggered.
SET #rows_being_deleted = NULL;
-- throw exception
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Cannot delete more than one order per time!';
END IF;
END //
DELIMITER ;
DB Fiddle Demo 1: Trying to delete more than row.
DELETE FROM `test` WHERE `id`< 5;
Result:
Query Error: Error: ER_SIGNAL_EXCEPTION: Cannot delete more than one
order per time!
DB Fiddle Demo 2: Trying to delete only one row
Query #1
DELETE FROM `test` WHERE `id` = 1;
Deletion successfully happened. We can check the remaining rows using Select.
Query #2
SELECT * FROM `test`;
| id | a | b |
| --- | --- | --- |
| 2 | 3 | 4 |

Related

MySQL CHECK constraint to ensure record does not refer to itself with autoincrement id?

I have a SQL table that can reference another record in the table as its parent but should not reference itself. I have attempted to enforce this with a CHECK constraint but my attempts have failed as the id is an auto-increment column. Is there any other way to ensure that parent_id <> id?
My current attempt, which fails with error Check constraint 'not_own_parent' cannot refer to an auto-increment column. (errno 3818):
CREATE TABLE `content` (
`id` serial PRIMARY KEY NOT NULL,
`item_id` int NOT NULL,
`nested_item_id` int,
`block_id` int,
`order` int NOT NULL,
CONSTRAINT not_own_parent CHECK (nested_item_id <> id)
);
Here's a demo of using a trigger to cancel an insert that violates the condition you describe. You must use an AFTER trigger because in a BEFORE trigger the auto-increment value has not yet been generated.
mysql> delimiter ;;
mysql> create trigger t after insert on content
-> for each row begin
-> if NEW.nested_item_id = NEW.id then
-> signal sqlstate '45000' set message_text = 'content cannot reference itself';
-> end if;
-> end;;
mysql> delimiter ;
mysql> insert into content set item_id = 1, nested_item_id = 1, `order` = 1;
ERROR 1644 (45000): content cannot reference itself
mysql> insert into content set item_id = 1, nested_item_id = 2, `order` = 1;
Query OK, 1 row affected (0.01 sec)
Don't put this kind of thing in a constraint. For one thing, you can't do it directly in MySql. You'd have to use a trigger or something.
Instead:
write your CRUD code carefully, so it avoids generating incorrect rows. You have to do that anyway.
write a little program called "database_consistent" or something. Have it run a bunch of queries looking for any errors like the one you're trying to avoid. Have it send emails or SMSs if it finds problems. Run it often during development and at least daily in production.
One way to control auto-generated live values is by using triggers to manage new values.
For example, create instead of insert trigger to control newly generated ID. In triggers, you can make decisions based on the new value.

Cannot update value on insert if salary is greater than

I have a problem creating a trigger for a basic table that will check on insert if one of the values inserted is bigger than 3000 and replace it with 0. It throws this error:
Can't update table 'staff' in stored function/trigger because it is already used by statement which invoked this stored function/trigger
The structure of the table is very simple:
CREATE TABLE `staff` (
`ID` int(11) NOT NULL,
`NAZWISKO` varchar(50) DEFAULT NULL,
`PLACA` float DEFAULT NULL
)
And the trigger for it looks like this:
BEGIN
IF new.placa >= 3000 THEN
UPDATE staff SET new.placa = 0;
END IF;
END
I don't understand fully what occurs here, but I suspect some recursion, but I am quite new to the topic of triggers and I have lab coming, so I want to be prepared for it.
MySQL disallows triggers from doing UPDATE/INSERT/DELETE against the same table for which the trigger executed, because there is too great a chance of causing an infinite loop. That is, in UPDATE trigger, if you could UPDATE the same table, that would cause the UPDATE trigger to execute, which would UPDATE the same table, and so on and so on.
But I guess you only want to change the value of placa on the same row being handled by the trigger. If so, just SET it:
BEGIN
IF new.placa >= 3000 THEN
SET new.placa = 0;
END IF;
END
Remember that you must use a BEFORE trigger when changing column values.

Warning #1366 on phpMyAdmin INSERT even with a BEFORE INSERT TRIGGER code to resolve it. Natural?

Summary
When I use the phpMyAdmin GUI to insert a new entry into my table (which has a BEFORE INSERT TRIGGER applied to it), it seems to insert the entry just fine... but it always displays this in response:
( ! ) 1 row inserted.
Warning: #1366 Incorrect integer value: '' for column 'line_id' at row 1
What am I doing wrong? Is there a better way to set up the trigger so I don't get the error?
Background
Note: You can probably skip the code blocks as you read.
Using phpMyAdmin, I created my second table with this SQL statement. (This worked fine.)
CREATE TABLE IF NOT EXISTS `myDb`.`line_item` (
`order_id` INT NOT NULL,
`line_id` INT NOT NULL,
`line_text` VARCHAR(100) NOT NULL,
PRIMARY KEY (`order_id`, `line_id`),
CONSTRAINT `FK_order_line_item`
FOREIGN KEY (`order_id`)
REFERENCES `myDb`.`order` (`order_id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
You'll note there's no AUTO_INCREMENT imposed on line_id. That is because we want it to reset it's numbering with each new order_id. To accomplish this resetting number, we presumed a TRIGGER was most appropriate for the task.
When I tried to add a TRIGGER with this code, phpMyAdmin said it couldn't make it. (Something to do about "permissions" or such, but I quickly resorted to a built-in workaround after this little adventure.)
DELIMITER $$
USE `myDb`$$
CREATE DEFINER = CURRENT_USER TRIGGER `myDb`.`line_id_incrementer`
BEFORE INSERT ON `line_item`
FOR EACH ROW
BEGIN
DECLARE i INT;
SELECT COALESCE(MAX(line_id), 0) + 1
INTO i
FROM line_item
WHERE order_id = NEW.order_id;
SET NEW.line_id = i;
END$$
DELIMITER ;
When the above SQL statement didn't work, I simply used the GUI of phpMyAdmin to add the trigger to the table.
Server:localhost > Database:myDb > Table:line_item > "Triggers" Tab > "New" > "Add Trigger"
Trigger Name: line_id_incrementer
Table: line_item
Time: BEFORE
Event: INSERT
Definition:
BEGIN
DECLARE i INT;
SELECT COALESCE(MAX(line_id), 0) + 1
INTO i
FROM line_item
WHERE order_id = NEW.order_id;
SET NEW.line_id = i;
END
So far so fair.
Performing a practice run, I inserted a test entry into the 'order' table via the phpMyAdmin GUI (the 'Insert' Tab while viewing the 'order' table.): No problems there.
When I inserted a test entry for 'line_id' via the phpMyAdmin GUI, I left the NOT NULL 'line_id' field empty, to see if the trigger would fill it in correctly for me. And that's when I got this:
( ! ) 1 row inserted.
Warning: #1366 Incorrect integer value: '' for column 'line_id' at row 1
With the generated code shown as:
INSERT INTO `line_item`
(`order_id`, `line_id`, `line_text`)
VALUES ('1', '', 'This is a test line for the incrementer trigger');
What is interesting is: It inserted the entry as expected (with a 1 as the line_id). When I inserted a second entry, the warning still showed, but the next entry was also entered as expected (with a 2 as the line_id).
So, the rows seem to be inserted just fine, but I keep getting that nagging Warning which makes me suspect I didn't do something up to "Best Practice Standards".
The problem with your trigger is that theSELECT query will not return anything if there are no rows yet in the table for the current order. One solution is to move the COALESCE to the SET :
BEGIN
DECLARE i INT;
SELECT MAX(line_id) + 1
INTO i
FROM line_item
WHERE order_id = NEW.order_id;
SET NEW.line_id = COALESCE(i, 1);
END
I don't think your table definition is appropriate for what you wish to achieve. In particular the line_id and line_text are defined as not null. The problem with this is that the insert statement carries out the not null tests prior to the trigger being actioned and fails (I am assuming that your insert is only order_id and possibly line_text). You might want to look at setting default values. For example.
drop table if exists line_item;
CREATE TABLE `line_item` (
`order_id` INT NOT NULL,
`line_id` INT not null default 0,
`line_text` VARCHAR(100) not null default '',
PRIMARY KEY (`order_id`, `line_id`) #,
#CONSTRAINT `FK_order_line_item`
# FOREIGN KEY (`order_id`)
# REFERENCES `myDb`.`order` (`order_id`)
# ON DELETE NO ACTION
# ON UPDATE NO ACTION)
);
drop trigger if exists t;
delimiter $$
create trigger t before insert on line_item
for each row
BEGIN
DECLARE i INT;
SELECT MAX(line_id) + 1
INTO i
FROM line_item
WHERE order_id = NEW.order_id;
SET NEW.line_id = COALESCE(i, 1);
END $$
delimiter $$
insert into line_item (order_id) values (1),(1);
Query OK, 2 rows affected (0.05 sec)
Records: 2 Duplicates: 0 Warnings: 0
+----------+---------+-----------+
| order_id | line_id | line_text |
+----------+---------+-----------+
| 1 | 1 | |
| 1 | 2 | |
+----------+---------+-----------+
2 rows in set (0.00 sec)
I don't use phpmyadmin and have no idea why it shows the warning it does but I suggest you run the insert from command line sql to see what error it throws.

MySQL: Update second table with trigger and IF statement

I'm trying to create a trigger which sets selectoin allow to 1 when main state is updated to 3. However I can't get the query to work correctly. So far I have:
create table main
(main_id varchar(30) primary key,
name varchar(30) null,
state int null,
update_timestamp timestamp null);
create table selection
(id varchar(30) primary key,
allow varchar(30) null,
last_update_timestamp timestamp null);
//
create trigger upd_selectoin
before update on main
for each row
IF new.state = 3
THEN
UPDATE selection s
JOIN main m
ON m.main_id = s.id
SET s.allow = 1
WHERE s.id = NEW.main_id;
END IF;
END;
//
insert into main values (1,'row1',1,null);
insert into main values (2,'row2',0,null);
insert into selection values (1,null,null);
insert into selection values (2,null,null);
Error message:
Schema Creation Failed: 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 '//
You have at least two problems with the syntax:
You meant to use DELIMITER //
You forgot BEGIN
The correct definition might look like
DELIMITER //
CREATE TRIGGER upd_selectoin
BEFORE UPDATE ON main
FOR EACH ROW
BEGIN
IF NEW.state = 3 THEN
UPDATE selection s JOIN main m
ON m.main_id = s.id
SET s.allow = 1
WHERE s.id = NEW.main_id;
END IF;
END //
DELIMITER ;
Here is SQLFiddle demo
Now in your case
You probably want to use AFTER event instead of BEFORE to make sure that update in main has been made successfully before you make any updates to selection.
Also there is no point in JOINing selection with main since you already know main_id.
You can utilize INSERT ... ON DUPLICATE KEY UPDATE to make sure that a row in selection will exist even if it wasn't before
Looking at the selection schema you probably meant to assign a timestamp during that update
That being said a more succinct version of your trigger might look like
CREATE TRIGGER upd_selection
AFTER UPDATE ON main
FOR EACH ROW
INSERT INTO selection (id, allow, last_update_timestamp)
VALUES (NEW.main_id, IF(NEW.state = 3, 1, NULL), NOW())
ON DUPLICATE KEY UPDATE allow = VALUES(allow),
last_update_timestamp = VALUES(last_update_timestamp);
This version of a trigger doesn't even need DELIMITER and BEGIN ... END block because it contains only one statement.
Here is SQLFiddle demo

Creating an immutable field in mysql

I'd like to make a TIMESTAMP field DEFAULT CURRENT_TIMESTAMP, for 'creation time' purpose. But if someone or somehow something changes that TIMESTAMP, my data won't be consistent.
Is there a way I can ensure it won't change unless I delete the row and reinsert it, other than application level?
With the suggested answer provided, i could work around with something like this
CREATE TRIGGER consistency1 BEFORE UPDATE ON table1
FOR EACH ROW
BEGIN
IF NEW.creationtime != OLD.creationtime THEN
SET NEW.creationtime = OLD.creationtime;
END IF;
END;
Since my comment has been appreciated, here's the extended version.
I personally don't think that it's possible.
Anyway, there are a couple of things you can try:
Make sure that only your application can write on the database
Write a trigger like this (pseudocode!)
create trigger prevent_change_timestamp on tbl_name
before update
#fetch old row value
#verify if the timestamp field has been changed
#raise an error (any SQL error will do)
Or like this
create trigger revert_change_timestamp on tbl_name
after update
#fetch pre-change row value
#update the row with the "old" value in place of the new one
I'd personally go with the 3rd option, if possible. Anyway, the 2nd one is good too. I'd not rely on the 1st option unless necessary (eg: no access to trigger functionality)
More info here: reference
It's funny in a way that database apps don't offer this functionality as standard: not only for a "created" timestamp field, but for things like autoincrement id fields, and any miscellaneous values which you may want to set on creating a record and then never allow to be changed... wonder what the rationale is?
What you can do here is, you can write a TRIGGER on the table when a row is being updated. In that trigger, you can compare the old and new values, and if they are different then you can just overwrite the new value with the old one.
I tried this in MySQL 5.1 and got an error
DELIMITER //
CREATE TRIGGER member_update_0
-> AFTER UPDATE ON members
-> FOR EACH ROW
-> BEGIN
-> IF NEW.id != OLD.id THEN
-> SET NEW.id = OLD.id;
-> END IF;
-> END;//
ERROR 1362 (HY000): Updating of NEW row is not allowed in after trigger
The same trigger with AFTER replaced by BEFORE is accepted;
to me, this is a counter-intuitive way to do it, but it works
delimiter ;
UPDATE members SET id=11353 WHERE id=1353;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1 Changed: 0 Warnings: 0
It is actually possible to do this very neatly if you are using InnoDB.
Create another table with just one column. That column should have a foreign key (hence the innodb requirement in this solution) that points to the immutable column of the original table in question.
Put a restriction like "ON UPDATE RESTRICT".
In summary:
CREATE TABLE original (
....
immutable_column ...
INDEX index1(immutable_column)
....
) ENGINE=INNODB;
CREATE TABLE restricter (
.....
col1,
INDEX index2(col1),
FOREIGN KEY (col1) REFERENCES original (immutable_colum) ON UPDATE RESTRICT ON DELETE CASCADE
) ENGINE=INNODB;
Taking the idea a step further (for those of us still stuck with a legacy version of MySQL) you can have BOTH a protected & defaulted create_stamp AND an auto-updating update_stamp as follows:
If you have a table such as
CREATE TABLE `csv_status` (
`id` int(11) NOT NULL primary key AUTO_INCREMENT,
`create_stamp` datetime not null,
`update_stamp` timestamp default current_timestamp on update current_timestamp,
`status` enum('happy','sad') not null default 'happy'
);
Then you can define these triggers on it
drop trigger if exists set_create_stamp ;
create definer = CURRENT_USER trigger set_create_stamp BEFORE INSERT on
csv_status for each row
set NEW.create_stamp = now();
drop trigger if exists protect_create_stamp ;
delimiter //
create definer = CURRENT_USER trigger protect_create_stamp BEFORE UPDATE on
csv_status for each row
begin
if NEW.create_stamp != OLD.create_stamp then
set NEW.create_stamp = OLD.create_stamp;
end if;
end;//
delimiter ;