Mirror tables: triggers, deadlock and implicit commits - mysql

I'm have 2 similar tables, for example A and B. I want to replicate insertions in A to B, and insertions in B to A to integrate two users systems . I configured "after insert triggers" on each one. Example:
DELIMITER $$
CREATE DEFINER = `root`#`localhost` TRIGGER
`after_A_INSERT`
AFTER INSERT ON `A`
FOR EACH ROW BEGIN
INSERT INTO `B`
SET `id` = NEW.`id`,`name` = NEW.`name`;
END$$
DELIMITER ;
DELIMITER $$
CREATE DEFINER = `root`#`localhost` TRIGGER
`after_B_INSERT`
AFTER INSERT ON `B`
FOR EACH ROW BEGIN
INSERT INTO `A`
SET `id` = NEW.`id`,`name` = NEW.`name`;
END$$
DELIMITER ;
If I insert in A, the triggers calls an insert in B, but this insert executes the trigger in B and a deadlock occurs avoiding a infinite loop.
I've tried to edit triggers to DROP the another table trigger before do the INSERT and then CREATE it again after it. Example:
DELIMITER $$
CREATE DEFINER = `root`#`localhost` TRIGGER
`after_B_INSERT`
AFTER INSERT ON `B`
FOR EACH ROW BEGIN
DROP TRIGGER IF EXISTS `after_A_INSERT`;
INSERT INTO `A`
SET `id` = NEW.`id`, `name` = NEW.`name`;
/* And CREATE again here */
END$$
DELIMITER ;
However CREATE is a Data Definition Language (DDL) statement that makes an implicit commit. Thus, this can't be done.
I've tried to call a PROCEDURE with the DROP inside to handle explicitly the commit, but isn't possible too.
Any suggestion to mirror this 2 tables?
UPDATE: Using Bill Karwin suggestion, I added a origin field to each table with a respective default vale A or B. Then, I alter (DROP and reCREATE) the triggers as follows:
Trigger in A:
...
BEGIN
IF NEW.`origin`='A' THEN
INSERT INTO `B`
SET `id` = NEW.`id`, `name` = NEW.`name`, `origin` = NEW.`origin`;
END IF;
END
Trigger in B:
...
BEGIN
IF NEW.`origin`='B' THEN
INSERT INTO `A`
SET `id` = NEW.`id`, `name` = NEW.`name`, `origin` = NEW.`origin`;
END IF;
END

You need some way to avoid creating a cycle.
I'd suggest adding a column origin in both tables. In table A, make the DEFAULT 'A'. In table B, make the DEFAULT 'B'.
When inserting to either table in your application, always omit the origin column, allowing it to take its default value.
In both triggers, replicate to the other table only if the NEW.origin is equal to the respective table's default.
Re your comment and new error:
Sorry, I forgot to mention that in the trigger when inserting to the other table, you must also copy the value of NEW.origin. Just in your application when you do the original insert do you omit origin.
Trigger in A:
...
BEGIN
IF NEW.`origin`='A' THEN
INSERT INTO `B`
SET `id` = NEW.`id`, `name` = NEW.`name`, `origin` = NEW.`origin`;
END IF;
END
Trigger in B:
...
BEGIN
IF NEW.`origin`='B' THEN
INSERT INTO `A`
SET `id` = NEW.`id`, `name` = NEW.`name`, `origin` = NEW.`origin`;
END IF;
END
I created these triggers and then tested:
mysql> insert into A set name = 'bill';
Query OK, 1 row affected (0.00 sec)
mysql> insert into B set name = 'john';
Query OK, 1 row affected (0.01 sec)
mysql> select * from A;
+----+------+--------+
| id | name | origin |
+----+------+--------+
| 1 | bill | A |
| 2 | john | B |
+----+------+--------+
2 rows in set (0.00 sec)
mysql> select * from B;
+----+------+--------+
| id | name | origin |
+----+------+--------+
| 1 | bill | A |
| 2 | john | B |
+----+------+--------+

Related

MariaDB and stored procedure: Why does the cursor iterate the last row twice?

MariaDB and stored procedure: Why does the cursor iterate the last row twice using?
I have a table tab which for simplicity contains only one column id with numbers from 1 to 3 (see part 1 of the code below). I created a cursor (3) to retrieve each number from tab and entered the number as IN parameter into a short procedure (sp, part 3). The sp inserts the parameter into the table tab_ctrl (ctrl for controlling).
After runnig the cursor (4) and selecting the content of tab_ctrl I got this result:
+----+
| id |
+----+
| 1 |
| 2 |
| 3 |
| 3 | <- why?
+----+
The last row of tab has been iterated twice. Why the cursor doesn't stop after the third row?
-- **************************************
-- (1) Prepare tables
-- **************************************
use test0;
-- Table to read each entry
DROP TABLE IF EXISTS tab;
CREATE TABLE tab (
id INTEGER UNSIGNED DEFAULT NULL
, PRIMARY KEY (id)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
;
INSERT INTO tab VALUES
(1)
,(2)
,(3)
;
-- Table for controlling
DROP TABLE IF EXISTS tab_ctrl;
CREATE TABLE tab_ctrl (
id INTEGER UNSIGNED DEFAULT NULL
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
;
-- **************************************
-- (4) Run sp
-- **************************************
CALL sp_cursor_add_records();
SELECT * FROM tab_ctrl;
-- **************************************
-- (2) Stored procedure
-- **************************************
DELIMITER //
DROP PROCEDURE IF EXISTS sp_add_records //
CREATE PROCEDURE sp_add_records(IN p_id INTEGER UNSIGNED)
BEGIN
INSERT INTO tab_ctrl
SELECT p_id
;
END//
DELIMITER ;
-- **************************************
-- (3) Cursor
-- **************************************
DELIMITER //
DROP PROCEDURE IF EXISTS sp_cursor_add_records //
CREATE PROCEDURE sp_cursor_add_records()
BEGIN
-- Local variables
DECLARE done BOOLEAN DEFAULT 0;
DECLARE p_id INTEGER UNSIGNED;
-- Cursor
DECLARE c CURSOR
FOR
SELECT id
FROM tab
ORDER BY id
;
-- Declare continue handler
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done=1;
-- Open cursor
OPEN c;
-- Loop through all rows
REPEAT
-- Get record
FETCH c INTO p_id;
-- Call add record procedure
CALL sp_add_records(p_id);
-- End of loop
UNTIL done END REPEAT;
-- Close cursor
CLOSE c;
END//
DELIMITER ;
If you restructure your loop to only CALL sp_ad_records(p_id) if its not done:
-- Declare continue handler
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done=1;
-- Open cursor
OPEN c;
-- Get First record
FETCH c INTO p_id;
-- Loop through all rows
WHILE NOT done DO
-- Call add record procedure
CALL sp_add_records(p_id);
-- Get next record
FETCH c INTO p_id;
-- End of loop
END WHILE;
-- Close cursor
CLOSE c;

MySQL trigger end if syntax error

Unable to create trigger on update of specific field
delimiter //
CREATE TRIGGER `add_event` AFTER UPDATE ON `order_table`
FOR EACH ROW
IF NEW.status <=> OLD.status THEN
INSERT INTO `events` SET
events.status = NEW.status,
events.order_id = NEW.order_id,
events.time_stamp = CURRENT_TIMESTAMP
END IF;
delimiter ;
I am getting this error
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 'END IF;
There is no syntax error in below mysql trigger.
delimiter //
CREATE TRIGGER add_event AFTER UPDATE ON order_table
FOR EACH ROW
BEGIN
IF NEW.status <=> OLD.status THEN
INSERT INTO `events`
SET events.status = NEW.status,
events.order_id = NEW.order_id,
events.time_stamp = CURRENT_TIMESTAMP;
END IF;
END;//
delimiter ;
drop table if exists t;
drop table if exists events;
create table t(order_id int, status varchar(1));
create table events(order_id int, time_stamp timestamp,status varchar(1));
drop trigger if exists t;
delimiter //
CREATE TRIGGER t after update ON `t`
FOR EACH ROW
begin
IF NEW.status <> OLD.status THEN
INSERT INTO `events`
SET events.status = NEW.status,
events.order_id = NEW.order_id,
events.time_stamp = CURRENT_TIMESTAMP;
END IF ;
end //
delimiter ;
insert into t values(1,1);
insert into events values(1,now(),1);
update t set status = 2 where order_id = 1;
select * from t;
select * from events;
MariaDB [sandbox]> select * from t;
+----------+--------+
| order_id | status |
+----------+--------+
| 1 | 2 |
+----------+--------+
1 row in set (0.00 sec)
MariaDB [sandbox]> select * from events;
+----------+---------------------+--------+
| order_id | time_stamp | status |
+----------+---------------------+--------+
| 1 | 2018-08-06 10:16:35 | 1 |
| 1 | 2018-08-06 10:16:35 | 2 |
+----------+---------------------+--------+
2 rows in set (0.00 sec)
Why are you using if statement. You are checking everything <=>. Here is the solution :
CREATE TRIGGER `add_event` AFTER UPDATE ON `order_table`
FOR EACH ROW
BEGIN
INSERT INTO `events` SET events.status = NEW.status, events.order_id = NEW.order_id, events.time_stamp = CURRENT_TIMESTAMP;
END;
delimiter ;

value of variable returned from a query (with empty result set)?

I have a mysql query written as part of a Stored Procedure
-- {parameter}
SELECT id INTO emailId FROM email_list WHERE email = {email};
If this query returns an empty result set, what is the value of emailId? undefined, null, 0
I want to check if emailId contains a value and run a series of inserts.
I could use COUNT or IFNULL but that would mean declaring another variable to check against. I want to avoid it if possible.
***************************** EDIT ****************************
DELIMITER $$
CREATE DEFINER=`root`#`localhost` PROCEDURE `addCustomer`(
IN email1 VARCHAR(60),
IN status1 VARCHAR(15),
IN bill_pin_id SMALLINT UNSIGNED,
IN bill_addr VARCHAR(175),
IN bill_name VARCHAR(70),
IN tel VARCHAR(15)
)
BEGIN
DECLARE emailId INT UNSIGNED DEFAULT 0;
DECLARE billAddrId INT UNSIGNED DEFAULT 0;
DECLARE custId INT UNSIGNED DEFAULT 0;
DECLARE orderId INT UNSIGNED DEFAULT 0;
DECLARE sqlError TINYINT DEFAULT FALSE;
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET sqlError = true;
-- get id of email if it exists
SELECT id INTO emailId FROM email_list WHERE email = email1;
-- if email exists, check if a customer is listed
IF (emailId > 0) THEN
SELECT id INTO custId FROM customer WHERE email_list_id = emailId;
IF (custId > 0) THEN
-- check for duplicate address in the address table
SELECT address_id INTO billAddrId FROM customer_addr
INNER JOIN address ON address_id = address.id
WHERE customer_id = custId AND address = bill_addr;
END IF;
END IF;
START TRANSACTION;
-- if emails isnt listed - no customer or address should exist
IF (emailId = 0) THEN
INSERT INTO email_list (email, status) VALUES (email1, status1);
SELECT LAST_INSERT_ID() INTO emailId;
SELECT emailId;
INSERT INTO customer (email_list_id, full_name, phone) VALUE (emailId, bill_name, tel);
SELECT LAST_INSERT_ID() INTO custId;
SELECT custId;
INSERT INTO address (pincode_id, address) VALUES (bill_pin_id, bill_addr);
SELECT LAST_INSERT_ID() INTO billAddrId;
SELECT billAddrId;
INSERT INTO customer_addr (address_id, customer_id) VALUES (billAddrId, custId);
SELECT LAST_INSERT_ID();
INSERT INTO orders (customer_id, order_status) VALUES (custId, 'pending');
SELECT LAST_INSERT_ID() INTO orderId;
END IF;
IF sql_error = FALSE THEN
COMMIT;
SELECT 'SUCCESS';
ELSE
ROLLBACK;
SELECT 'FAILED';
END IF;
END$$
DELIMITER ;
I removed the unnecessary lines in the code to keep it short.
Here is the values returned from the select statements. All tables are empty. But the below values indicate rows have been inserted. But it is still empty.
emailId
43
custId
12
billAddrId
13
CustomerAddr
13
orderId
8
You can test it by selecting the value:
drop procedure if exists test_proc;
create procedure test_proc()
begin
DECLARE emailId INT UNSIGNED;
SET emailId = 0;
SELECT 123 INTO emailId FROM dual WHERE 1 = 2;
SELECT emailId;
end;
call test_proc();
This will return 0, which means - the value is not changed if no row has been found.
Demo: http://rextester.com/MHRTL99697
Works for me
MariaDB [sandbox]> drop procedure if exists p;
Query OK, 0 rows affected (0.06 sec)
MariaDB [sandbox]> delimiter $$
MariaDB [sandbox]> create procedure p(inid int)
-> begin
-> declare vid int;
-> set vid = 0;
-> select id into vid from users where id = inid;
-> if vid = 1 then
-> select 'found';
-> else
-> select 'notfound';
-> end if;
-> end $$
Query OK, 0 rows affected (0.05 sec)
MariaDB [sandbox]> delimiter ;
MariaDB [sandbox]>
MariaDB [sandbox]> call p(1);
+-------+
| found |
+-------+
| found |
+-------+
1 row in set (0.02 sec)
Query OK, 0 rows affected (0.02 sec)
MariaDB [sandbox]>
MariaDB [sandbox]> call p(1000);
+----------+
| notfound |
+----------+
| notfound |
+----------+
1 row in set (0.00 sec)
Query OK, 0 rows affected, 1 warning (0.01 sec)
MariaDB [sandbox]> show warnings;
+---------+------+-----------------------------------------------------+
| Level | Code | Message |
+---------+------+-----------------------------------------------------+
| Warning | 1329 | No data - zero rows fetched, selected, or processed |
+---------+------+-----------------------------------------------------+
1 row in set (0.00 sec)

SQL : How to update table Automatically in 2nd database when table in 1st database is updated?

I have database db1 and db2 having the table mytable on both the databases in same server.
Both the table contains exactly the same columns.
For Ex:
SNo | fname | lname | Mobile | Status
Both the table has a column name Status
What should I do so that when value of column Status is updated in table mytable in database db1 then value of column Status is updated AUTOMATICALLY in table mytable in database db2
I don't know what actually it is called. Perhaps Trigger !
You have to create a trigger in Mysql.
Example:
CREATE TRIGGER upd_check BEFORE UPDATE ON account
-> FOR EACH ROW
-> BEGIN
-> IF NEW.amount < 0 THEN
-> SET NEW.amount = 0;
-> ELSEIF NEW.amount > 100 THEN
-> SET NEW.amount = 100;
-> END IF;
-> END;
I am using the above trigger to update two tables.
Have a look on this tutorial
update
You can not trigger on a particular column update in SQL, it is applied on a row.
You can put your condition for column inside your trigger like below
DELIMITER $$
CREATE TRIGGER myTrigger AFTER UPDATE ON db1.mytable
FOR EACH ROW
BEGIN
if NEW.Status <> OLD.Status
then
update db2.mytable set Status = NEW.Status where sno = OLD.sno;
END if;
END $$

MySQL stored procedure giving different results to command line

I have a simple MySQL stored procedure designed to return all child records for a given node.
My problem is when I type this manually it returns the proper results - but when I put the same code into a Stored Procedure it returns only the parent id.
I'd really appreciate some guidance!
For example - when I call my procedure (code is below) I get:
call find_child(1006);
+--------+
| nodeid |
+--------+
| 1006 |
| 1006 |
| 1006 |
| 1006 |
+--------+
4 rows in set (0.01 sec)
BUT - When I cut and paste the command I get the proper resultset:
mysql> create temporary table KID_TABLE (nodeid INT);
Query OK, 0 rows affected (0.00 sec)
mysql> insert ignore into KID_TABLE (nodeid) select nodeid from CORPORATENODE
where parentid in (1006);
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0
mysql> select * from KID_TABLE;
+--------+
| nodeid |
+--------+
| 1007 |
| 1008 |
| 1031 |
| 1038 |
+--------+
4 rows in set (0.00 sec)
Here's the code:
DELIMITER $$
DROP PROCEDURE IF EXISTS `find_child`$$
CREATE PROCEDURE `find_child`( IN NodeID INT)
DETERMINISTIC
BEGIN
declare nid INT;
set nid= NodeID;
create temporary table KID_TABLE (nodeid INT);
insert ignore into KID_TABLE (nodeid) select nodeid
from CORPORATENODE where parentid in (1006);
select * from KID_TABLE;
drop table KID_TABLE;
END $$
DELIMITER ;
Here's the DDL for PARENT table
CREATE TABLE `PARENT` (
`NODEID` int(11) NOT NULL AUTO_INCREMENT,
`PARENTID` int(11) NOT NULL DEFAULT '0' COMMENT '0 value means top node',
`NAME` varchar(50) NOT NULL,
PRIMARY KEY (`NODEID`) USING BTREE
) ENGINE=InnoDB;
Mysql will return your in variable NodeId in your select nodeid
from CORPORATENODE where parentid in (1006);
Change the variable to in_NodeID instead.
DELIMITER $$
DROP PROCEDURE IF EXISTS `find_child`$$
CREATE PROCEDURE `find_child`( IN in_NodeID INT)
BEGIN
create temporary table KID_TABLE (nodeid INT);
insert ignore into KID_TABLE (nodeid) select nodeid
from CORPORATENODE where parentid in (in_NodeID);
select * from KID_TABLE;
drop table KID_TABLE;
END $$
DELIMITER ;
But then of course, why use a temporary table?
DELIMITER $$
DROP PROCEDURE IF EXISTS `find_child`$$
CREATE PROCEDURE `find_child`( IN in_NodeID INT)
BEGIN
select nodeid from CORPORATENODE where parentid in (in_NodeID);
END $$
DELIMITER ;
Thanks Andreas et al,
I found a workaround to this just after posting - typical.
But couldn't post my solution till now.
I use Linux but have lower_case_table_names=1 set so the case is irrelevant. (I think?)
Andreas, I needed the temporary table as once I had selected back all the child records for a Node I then needed to update all the child records in the table NODEVERSION. I was running this via a trigger on the NODEVERSION table and was running into problems as I was updating the calling table etc. That's why I created a new CHILD_TABLE.
So I found that (even leaving DETERMINISTIC in... sorry pilcrow!)
The following worked as expected - all I did was added aliases ...
(The code below is a little different from above as I simplified the above code for my question)
Don't fully understand why the aliases fixed the initial problem - but it worked. Perhaps you experts know why?
CREATE PROCEDURE `find_child`( IN NodeID int)
DETERMINISTIC
BEGIN
declare nid INT;
declare rows_before INT DEFAULT 0;
declare rows_after INT DEFAULT 0;
set nid= NodeID;
delete from CHILD_TABLE;
insert into CHILD_TABLE values (nid);
set rows_before = (select count(*) from CHILD_TABLE);
if rows_before !=rows_after then
increment: repeat
set rows_before = (select count(*) from CHILD_TABLE);
insert ignore into CHILD_TABLE (nodeid) select b.nodeid from CORPORATENODE b
where b.parentid in (select c.nodeid from CHILD_TABLE c);
set rows_after= (select count(*) from CHILD_TABLE);
until rows_before =rows_after
end repeat increment;
end if;
update NODEVERSION n set STATUS=1 where n.nodeid in
(select c.nodeid from CHILD_TABLE c);
END