MySQL: Custom row validation - mysql

I have such example:
CREATE TABLE a(
id INT PRIMARY KEY AUTO_INCREMENT,
parent_id INT,
FOREIGN KEY (parent_id) REFERENCES a(id)
);
DELIMITER ;;
CREATE TRIGGER a_insert BEFORE INSERT ON a
FOR EACH ROW
BEGIN
SIGNAL SQLSTATE '01431' SET MESSAGE_TEXT = 'The foreign data source you are trying to reference does not exist.';
END;;
DELIMITER ;
INSERT INTO a(parent_id) VALUES (NULL);
INSERT INTO a(parent_id) VALUES (1);
INSERT INTO a(parent_id) VALUES (2);
INSERT INTO a(parent_id) VALUES (4);
INSERT INTO a(parent_id) VALUES (999);
SELECT * FROM a
This end up with 4 recods:
----------------
id parent_id
----------------
1 NULL
2 1
3 2
4 4
I found post online that MySQL does not support rollbacks in triggers. That is a problem because I want such hierarchy where no row points to it self and inserts like the one with ID=4 works just fine. How do I ensure there are no such records in database?

Well, the problem is with auto_increment, because you in BEFORE INSERT event you don't have that value assigned yet. On the other hand in AFTER INSERT event you can't do anything with it.
If you want to use auto_increment id column a possible solution is to use separate table for sequencing.
Your schema would look like
CREATE TABLE a_seq(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
);
CREATE TABLE a(
id INT NOT NULL PRIMARY KEY DEFAULT 0,
parent_id INT,
FOREIGN KEY (parent_id) REFERENCES a(id)
);
And your trigger
DELIMITER $$
CREATE TRIGGER a_insert
BEFORE INSERT ON a
FOR EACH ROW
BEGIN
INSERT INTO a_seq VALUES(NULL);
SET NEW.id = LAST_INSERT_ID();
IF NEW.id = NEW.parent_id THEN
SET NEW.id = NULL;
END IF;
END$$
DELIMITER ;
If id=parent_id the trigger deliberately violates NOT NULL constraint assigning NULL value. Therefore this record won't be inserted.
Here is SQLFiddle demo. Uncomment last insert statement. It won't allow you to make such insert.

Related

How could I update the LAST_INSERT_ID in Mysql Trigger

In my MySQL Database, I have a table with a composite primary key where the ID is not in auto_increment mode. Something like this :
CREATE TABLE table_a (
fk_table_b INT UNSIGNED NOT NULL,
id INT UNSIGNED,
label VARCHAR(80) NOT NULL,
PRIMARY KEY (fk_table_b, id),
FOREIGN KEY fk_table_b
REFERENCES table_b(id)
);
To increment the ID in function of the foreign key, I made a trigger like this :
DELIMITER $$
CREATE TRIGGER table_a_auto_increment
BEFORE INSERT ON table_a
FOR EACH ROW BEGIN
SET NEW.id = (
SELECT IFNULL(MAX(id), 0) + 1
FROM table_a
WHERE table_a.fk_table_b = NEW.fk_table_b
);
END $$
DELIMITER ;
But when I do SELECT LAST_INSERT_ID() I am getting 0 as the new id ... Normally you could override the LAST_INSERT_ID() by giving it a number like this :
INSERT table_a ( fk_table_b, id)
VALUES (1, LAST_INSERT_ID(5));
SELECT LAST_INSERT_ID(); -- -> it gives me 5
So I have tried to combine both to do this trigger :
DELIMITER $$
CREATE TRIGGER table_a_auto_increment
BEFORE INSERT ON table_a
FOR EACH ROW BEGIN
SET NEW.id = (
SELECT LAST_INSERT_ID(IFNULL(MAX(id), 0) + 1)
FROM table_a
WHERE table_a.fk_table_b = NEW.fk_table_b
);
END $$
DELIMITER ;
But it's still giving me 0 when I insert something in the base ... Do you know if there is a way to make it work ?
Thanks a lot.
-- EDIT 2020-08-14
Finally it seems impossible to override the LAST_INSERT_ID function inside the TRIGGER, so I changed my solution by removing the trigger and doing it inside my insert function like this :
INSERT table_a ( fk_table_b, id, label)
VALUES (1, LAST_INSERT_ID((
SELECT IFNULL(MAX(old_one.id), 0) + 1
FROM table_a AS old_one
WHERE old_one.fk_table_b = table_a.fk_table_b
)), "something");
And then, this is giving me the good result I can use in my backend :)
You may use additional service table:
CREATE TABLE service_table (id BIGINT AUTO_INCREMENT PRIMARY KEY);
and
DELIMITER $$
CREATE TRIGGER table_a_auto_increment
BEFORE INSERT ON table_a
FOR EACH ROW
BEGIN
SET NEW.id = (
SELECT IFNULL(MAX(id), 0) + 1
FROM table_a
WHERE table_a.fk_table_b = NEW.fk_table_b
);
DELETE FROM service_table WHERE id IS NOT NULL;
INSERT INTO service_table VALUES (NEW.id - 1);
INSERT INTO service_table VALUES (NULL);
SET NEW.id = LAST_INSERT_ID() - 1;
END $$
DELIMITER ;
fiddle (foreign key removed).
Maybe the code may be simplified a little - do it yourself.
Service table may be defined as Engine = MEMORY (if available).
The code is not safe for concurrent inserts.

Cannot create trigger

I am just want to create after insert trigger to insert a new row in history table. Why am I getting an error when I run the query?
orders
create table orders
(
id int auto_increment
primary key,
id_user int not null,
picture_name varchar(100) not null,
time date not null,
constraint FK_USER
foreign key (id_user) references stef.users (id)
)
;
create index FK_USER_idx
on orders (id_user)
;
history
create table history
(
id int auto_increment
primary key,
id_order int not null,
id_action int not null,
time date not null,
constraint FK_ORDER
foreign key (id_order) references stef.orders (id),
constraint FK_ACTION
foreign key (id_action) references stef.actions (id)
)
;
create index FK_ORDER_idx
on history (id_order)
;
create index FK_ACTION_idx
on history (id_action)
;
my trigger...
CREATE TRIGGER orders_AFTER_INSERT
AFTER INSERT ON stef.orders
FOR EACH ROW
BEGIN
INSERT INTO history('id_order', 'id_action', 'time')
VALUES (NEW.id, 1, NOW());
END;
I am just want to create after insert trigger to insert a new row in history table. Why am I getting an error when I run the query?
Try this
DELIMITER $$
CREATE TRIGGER orders_AFTER_INSERT
AFTER INSERT ON stef.orders
FOR EACH ROW
BEGIN
INSERT INTO history(`id_order`, `id_action`, `time`)
VALUES (NEW.id, 1, NOW());
END$$
DELIMITER ;
You need to temporarily override the delimiter so MySQL can differentiate between the end of a statement within the body of a trigger (or procedure, or function) and the end of the body.
Edit: Single quotes (') are only ever used to denote string values, for field names use the ` (or in some configurations the ")
CREATE TRIGGER orders_AFTER_INSERT
AFTER INSERT ON stef.orders
FOR EACH ROW
BEGIN
INSERT INTO stef.history()
VALUES (null, NEW.id, 1, NOW());
END

Atomic Upsert MySQL

What is the best method to atomically update a row if exists, else insert a new row into an table?
e.g. say we have this example below.
CREATE TABLE Test(
id integer,
val varchar(255)
)
DELIMITER $$
DROP PROCEDURE IF EXISTS `TestUpsert` $$
CREATE PROCEDURE `TestUpsert` (in value1 varchar(255), in id1 int)
BEGIN
IF EXISTS(Select 1 from Test where id=id1) THEN
UPDATE Test set val = value1 where id = id1;
ELSE
insert into Test(id, val) VALUES (id1, value1);
END IF;
END $$
DELIMITER ;
What changes can we make to TestUpsert to make it atomic?
Use on duplicate key update:
CREATE PROCEDURE `TestUpsert` (
in in_value varchar(255),
in in_id int
)
BEGIN
insert into test(id, val)
values (in_id, in_val)
on duplicate key update val = in_val;
END $$
I should add that for this to work, test.id has to be declared as the primary key or unique.
I recommend you read about INSERT ... ON DUPLICATE KEY UPDATE.
See my lengthy answer about this statement here: https://stackoverflow.com/a/548570/20860

Insert using LAST_INSERT_ID

How would I go about inserting while incrementing and using LAST_INSERT_ID? I have a table and trigger as such:
CREATE TABLE A(
id int NOT NULL AUTO_INCREMENT,
name char(15),
PRIMARY KEY(id)
);
CREATE TABLE B(
id int NOT NULL,
name char(15),
FOREIGN KEY(id) REFERENCES A(id)
);
delimiter //
CREATE TRIGGER T
AFTER INSERT ON B
FOR EACH ROW
BEGIN
IF (NEW.name LIKE 'A') THEN
INSERT INTO A VALUES(LAST_INSERT_ID() + 1, 'A');
END IF;
END//
delimiter ;
I know that INSERT INTO B VALUES(LAST_INSERT_ID() + 1, 'A'); doesn't work if I have multiple inserts into B because LAST_INSERT_ID() when doing multiple-row inserts, LAST_INSERT_ID() will return the value of the first row inserted (not the last). How would I go about incrementing the ID so that LAST_INSERT_ID() returns the most recent ID from the inserts`
If you want to insert the id from B, then use the id column:
CREATE TRIGGER T
AFTER INSERT ON B
FOR EACH ROW
BEGIN
IF (NEW.name LIKE 'A') THEN
INSERT INTO A VALUES(B.ID + 1, 'A');
END IF;
END//
This seems like a curious expression. Perhaps your real intention is simply to have an auto incremented id on both tables.
I realized that I could insert into A without id since its auto-incremented.

How to use trigger in MySql to make foreign key

I want to use trigger to make foreign key in MySql. I have the following tables:
1) 'content' table:
teacher_id varchar(20)
sub_id varchar(20)
path varchar(100)
file_name varchar(100)
2) 'teacher' table:
teacher_id varchar(20)
teacher_name varchar(45)
and I am using the following code for trigger(delimiter //):
CREATE TRIGGER fk_content_teacher_temp BEFORE INSERT ON `content`
FOR EACH ROW
BEGIN
DECLARE has_row TINYINT;
SET has_row = 0;
SELECT 1 INTO has_row FROM `teacher` INNER JOIN `content` ON content.teacher_id=teacher.teacher_id;
IF has_row=0 THEN
INSERT error_msg VALUES ('Foreign Key Constraint Violated!');
END IF;
END//
The problem is, when am trying to insert in content table for a teacher_id which is not present in teacher table, I get the following error:
1172 - Result consists of more than one row
What can I do to make it work fine, or any other way i can use trigger to make foreign keys?
Thank you in advance!
While it is not clear what exactly you intend with the statement "use trigger to make foreign key", your current issue is that SELECT INTO cannot be used in queries that return more than one result.
SELECT 1 INTO has_row FROM teacher INNER JOIN content ON content.teacher_id=teacher.teacher_id;
returns EVERY match between the two tables.
If you were trying to check if teacher contains the teacher_id value being used in the new content record, you should just be able to drop the JOIN clause completely and just query like so:
SELECT 1 INTO has_row FROM `teacher` WHERE `teacher_id` = NEW.`teacher_id`;
While this is an oldish question I would like to provide some insight for future searchers on how one might deal with such issue.
In a recent project I was unable to use InnoDB but had to use the MyISAM engine (in reality it was MariaDB's Aria engine) for a database transfer which contained foreign keys.
I opted for implementing foreign keys using triggers as described here.
A great intro into the subject is provided here: https://dev.mysql.com/tech-resources/articles/mysql-enforcing-foreign-keys.html
However, I will outline my solution as I found some thing not fully workable for me in the above. E.g. Any update to a parent table was completely prohibited in their "restrict" example when a foreign child key existed even though the child was not affected.
For demonstration I use the following table definitions and test data:
CREATE TABLE `__parent` (`id` int UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY (`id`)) ENGINE=`Aria`;
CREATE TABLE `__child` (`id` int UNSIGNED NOT NULL AUTO_INCREMENT,`parent_id` int UNSIGNED, PRIMARY KEY (`id`), INDEX `parent_id_idx` USING BTREE (`parent_id`) ) ENGINE=`Aria`;
INSERT INTO __parent VALUES (1), (2), (3);
INSERT INTO __child VALUES (1,1), (2,2), (3,1), (4,2), (5,3), (6,1);
Prevent inserts into a child table when no corresponding linked parent entry exists:
DELIMITER //
CREATE TRIGGER __before_insert_child BEFORE INSERT ON __child FOR EACH ROW
BEGIN
IF (SELECT COUNT(*) FROM __parent WHERE __parent.id=new.parent_id) = 0 THEN
SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 30001, MESSAGE_TEXT = 'Can\'t insert record. Foreign parent key does not exist!';
END IF;
END //
DELIMITER ;
Prevent updates to a child table where it would unlink a child record:
DELIMITER //
CREATE TRIGGER __before_update_child BEFORE UPDATE ON __child FOR EACH ROW
BEGIN
IF (SELECT COUNT(*) FROM __parent WHERE __parent.id = new.parent_id) = 0 THEN
SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 30001, MESSAGE_TEXT = 'Can\'t update record. Foreign parent key does not exist!';
END IF;
END //
DELIMITER ;
Cascading updates to the child table when the parent is updated:
DELIMITER //
CREATE TRIGGER __after_update_parent AFTER UPDATE ON __parent FOR EACH ROW
BEGIN
UPDATE __child SET __child.parent_id=new.id WHERE __child.parent_id=old.id;
END //
DELIMITER ;
Cascade deletes to the child table when a parent is deleted:
DELIMITER //
CREATE TRIGGER __after_delete_parent AFTER DELETE ON __parent FOR EACH ROW
BEGIN
DELETE FROM __child WHERE __child.parent_id=old.id;
END;
END //
DELIMITER ;
Sometime you don't want to cascade but restrict. In this case use the following instead:
Restrict parent updates to the child table:
DELIMITER //
CREATE TRIGGER __before_update_parent BEFORE UPDATE ON __parent FOR EACH ROW
BEGIN
IF ( old.id <> new.id AND (SELECT COUNT(*) FROM __child WHERE __child.parent_id = old.id) <> 0 ) THEN
SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 30001, MESSAGE_TEXT = 'Can\'t update record. Foreign key updates to child table restricted!';
END IF;
END //
DELIMITER ;
Restrict parent deletes from the child table:
DELIMITER //
CREATE TRIGGER __before_delete_parent BEFORE DELETE ON __parent FOR EACH ROW
BEGIN
IF ( SELECT COUNT(*) FROM __child WHERE __child.parent_id = old.id) <> 0 THEN
SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 30001, MESSAGE_TEXT = 'Can\'t delete record. Foreign key exists in child table!';
END IF;
END //
DELIMITER ;
Hope this helps someone.