WHILE Loop with insert not working as expected - mysql

I am try to find all child and sub-child of a given parent, but not getting desired output if I use WHILE loop. Surprisingly, if I execute the scripts manually, it works as expected. Please have a look at below script.
DROP TABLE IF EXISTS test;
CREATE TABLE test (id INT, parent_id INT);
INSERT INTO test VALUES (1,NULL);
INSERT INTO test VALUES (2,1);
INSERT INTO test VALUES (3,2);
INSERT INTO test VALUES (4,2);
INSERT INTO test VALUES (5,3);
INSERT INTO test VALUES (6,4);
INSERT INTO test VALUES (7,2);
INSERT INTO test VALUES (8,4);
INSERT INTO test VALUES (9,1);
INSERT INTO test VALUES (10,1);
COMMIT;
DELIMITER $$
USE `test`$$
DROP PROCEDURE IF EXISTS `get_parent_child_hierarchy`$$
CREATE PROCEDURE `get_parent_child_hierarchy`(Parent_ID INT)
BEGIN
DECLARE `rowcount` INT DEFAULT 1;
DROP TABLE IF EXISTS Temp;
CREATE TABLE Temp(ParentID INT, ChildID INT, Stage INT);
INSERT INTO Temp(ParentID, ChildID, Stage)
SELECT Parent_ID, Parent_ID, 2;
WHILE rowcount > 0 DO
UPDATE Temp
SET Stage = 1
WHERE Stage = 2;
COMMIT;
INSERT INTO Temp(ParentID, ChildID, Stage)
SELECT Parent_ID, id, 2
FROM Test T
WHERE Parent_id IN (SELECT ChildID
FROM Temp
WHERE Stage = 1
AND Temp.ChildID <> T.id);
UPDATE Temp
SET Stage = 0
WHERE Stage = 1;
COMMIT;
SET rowcount = (SELECT COUNT(ChildID)
FROM Temp
WHERE Stage = 2);
END WHILE;
SELECT *
FROM Temp
WHERE Parent_ID <> ChildID;
DROP TABLE IF EXISTS Temp;
END$$
DELIMITER ;
Input: 2
Expected Output:
ParentID | ChildID | Stage
---------|---------|------
2 | 3 | 0
2 | 4 | 0
2 | 5 | 0
2 | 6 | 0
2 | 7 | 0
2 | 8 | 0
But unfortunately, when the run the script using WHILE loop, I get all the rows in the input.
Any help is appreciated.
I agree this is moreover specific problem than issue with technical stuff. But I wonder if there is any issue with MySQL, when you use WHILE loop and INSERT.

You defined input parameter's name as Parent_ID which is a column in table test, this is reason that you did not get what you expected.
Try following;)
CREATE PROCEDURE `get_parent_child_hierarchy`(pid INT)
BEGIN
DECLARE `rowcount` INT DEFAULT 1;
DROP TABLE IF EXISTS Temp;
CREATE TABLE Temp(ParentID INT, ChildID INT, Stage INT);
INSERT INTO Temp(ParentID, ChildID, Stage)
SELECT pid, pid, 2;
WHILE rowcount > 0 DO
UPDATE Temp
SET Stage = 1
WHERE Stage = 2;
COMMIT;
INSERT INTO Temp(ParentID, ChildID, Stage)
SELECT pid, id, 2
FROM test T
WHERE parent_id IN (SELECT ChildID
FROM Temp
WHERE Stage = 1
AND Temp.ChildID <> T.id);
UPDATE Temp
SET Stage = 0
WHERE Stage = 1;
COMMIT;
SET rowcount = (SELECT COUNT(ChildID)
FROM Temp
WHERE Stage = 2);
END WHILE;
SELECT *
FROM Temp
WHERE pid <> ChildID;
DROP TABLE IF EXISTS Temp;
END

Related

Error Code: 1442. Can't update table 'A' in trigger because it is already used by statement which invoked this trigger

I have the following problem with mysql:
I have the table A with a trigger that update a columns of table B when something in A change. This trigger works.
I need te possibility to update a column of A when something in B change, but the second trigger generate the error.
I know is recursive, but how can I do it?
exp.
trigger A:
delimiter $$
CREATE TRIGGER TAU_A
AFTER UPDATE ON table_A FOR EACH ROW
begin
IF OLD.to_read <> NEW.to_read THEN
update table_B
set is_read=if(new.to_read=1,0,1)
where id=new.id;
END IF;
END$$
trigger B:
delimiter $$
CREATE TRIGGER TAU_B
AFTER UPDATE ON table_b FOR EACH ROW
begin
IF OLD.is_read <> NEW.is_readTHEN
update table_a
set to_read=if(new.is_read=1,0,1)
where id=new.id;
END IF;
END$$
Use user-defined variable, check for chaining update.
DEMO
#check_for_cycle user-defined variable is used for to prevent a cycle. The variable name must be unique over a system (i.e. it must be used in this triggers pack only) and provide interference absence (including another applications which may modify these tables data).
CREATE TABLE t1 (id INT PRIMARY KEY, val INT);
CREATE TABLE t2 (id INT PRIMARY KEY, val INT);
INSERT INTO t1 VALUES (1,1), (2,2);
INSERT INTO t2 VALUES (1,1), (3,3);
SELECT * FROM t1;
SELECT * FROM t2;
id
val
1
1
2
2
id
val
1
1
3
3
CREATE TRIGGER tr_au_t1
AFTER UPDATE ON t1
FOR EACH ROW
BEGIN
IF #check_for_cycle IS NULL THEN
SET #check_for_cycle := 1;
UPDATE t2 SET val = NEW.val + 10 WHERE id = NEW.id;
SET #check_for_cycle := NULL;
END IF;
END
CREATE TRIGGER tr_au_t2
AFTER UPDATE ON t2
FOR EACH ROW
BEGIN
IF #check_for_cycle IS NULL THEN
SET #check_for_cycle := 1;
UPDATE t1 SET val = NEW.val + 100 WHERE id = NEW.id;
SET #check_for_cycle := NULL;
END IF;
END
UPDATE t1 SET val = val + 1;
SELECT * FROM t1;
SELECT * FROM t2;
id
val
1
2
2
3
id
val
1
12
3
3
UPDATE t2 SET val = val + 2;
SELECT * FROM t1;
SELECT * FROM t2;
id
val
1
114
2
3
id
val
1
14
3
5
fiddle
You must check that no error occures in a trigger, and clear the variable in the error handler. Otherwise, when INSERT fails then the trigger breaks and the whole process rollbacks, but the variable value will stay non-cleared (variable assignment is not rollbacked) which will lock trigger action for further updates.

MySql error 1064 on trigger createion with if statement

Below is the code. All I want to do is check if a quote exists and, if not, insert the record into another table.
DROP TRIGGER IF EXISTS `CB2`;
CREATE TRIGGER CB2
AFTER UPDATE
ON `quotes` FOR EACH ROW
BEGIN
IF (SELECT quoteID FROM booking WHERE quoteID <> new.`quoteID`) THEN
INSERT INTO `booking`(`Book_ID`, `Date`, `CustomerID`, `CustodianID`, `cusCntNum`, `Service`, `sAddress`, `Size`, `Comments`, `Frequency`, `Duration`, `Bdrms`, `Bathrm`, `Living Spaces`, `AppointmentStartDate`, `Time`, `ServiceDay`, `AddOns`, `Fee`, `quoteID`, `uBookingID`) VALUES (NULL, CURRENT_DATE, new.CustAccNum, new.CustodianNum, new.Contact_Number, new.ServType,new.Address, new.CommercialSize, new.Comments, new.Frequency, new.Duration, new.Bedrooms, new.Bathrooms, new.lSpaces, new.AppointmentDate, new.AppointmentTime ,DAYOFWEEK(new.AppointmentTime), new.sAddOns, new.Fee, new.quoteID,'');
END IF;
END
You need DELIMITER so that mysql can identify whyt belongs to the trigger
DROP TRIGGER IF EXISTS `CB2`;
DELIMITER //
CREATE TRIGGER CB2
AFTER UPDATE
ON `quotes` FOR EACH ROW
BEGIN
IF (SELECT quoteID FROM booking WHERE quoteID <> new.`quoteID`) THEN
INSERT INTO `booking`
(`Book_ID`, `Date`, `CustomerID`, `CustodianID`, `cusCntNum`, `Service`, `sAddress`, `Size`, `Comments`, `Frequency`, `Duration`, `Bdrms`, `Bathrm`, `Living Spaces`, `AppointmentStartDate`, `Time`, `ServiceDay`, `AddOns`, `Fee`, `quoteID`, `uBookingID`) VALUES
(NULL, CURRENT_DATE, new.CustAccNum, new.CustodianNum, new.Contact_Number, new.ServType,new.Address, new.CommercialSize, new.Comments, new.Frequency, new.Duration, new.Bedrooms, new.Bathrooms, new.lSpaces, new.AppointmentDate, new.AppointmentTime ,DAYOFWEEK(new.AppointmentTime), new.sAddOns, new.Fee, new.quoteID,'');
END IF;
END//
DELIMITER ;
You do need to set delimiters but you also need an existence check. Using a simplified version of your model
drop table if exists quotes,booking;
create table quotes(quoteid int, val int);
create table booking(quoteid int,val int);
drop trigger if exists t;
delimiter $$
CREATE TRIGGER t
AFTER UPDATE
ON `quotes` FOR EACH ROW
BEGIN
insert into debug_table(msg) values (new.quoteid);
IF not exists (SELECT quoteID FROM booking WHERE quoteID = new.`quoteID`) THEN
INSERT INTO `booking`( `quoteID`,val) VALUES (new.quoteid,new.val);
END if;
end $$
delimiter ;
truncate debug_table;
insert into quotes(quoteid) values (1),(2);
update quotes set val = 10 where quoteid = 1;
update quotes set val = 20 where quoteid = 1;
MariaDB [sandbox]> select * from booking;
+---------+------+
| quoteid | val |
+---------+------+
| 1 | 10 |
+---------+------+
1 row in set (0.001 sec)
MariaDB [sandbox]>
MariaDB [sandbox]> select * from debug_table;
+----+------+
| id | msg |
+----+------+
| 1 | 1 |
| 2 | 1 |
+----+------+
2 rows in set (0.001 sec)
You don't need the debug table but it provides proof that the trigger fired twice as expected.
BTW I'm not convinced that your logic is sound.

Return SETOF rows from PostgreSQL function after WHILE LOOP section

I am trying to create a POSTGRESQL function which would first INSERT some data in a table using WHILE LOOP and then SELECT the results of this table.
This is an sql example:
CREATE OR REPLACE FUNCTION get_levels_test (maxlevel int) RETURNS SETOF list_of_levels AS $$
DECLARE
level int = 1;
BEGIN
TRUNCATE list_of_levels;
WHILE (level <= maxlevel) LOOP
INSERT INTO list_of_levels
SELECT level;
level = level + 1;
END LOOP;
select * from list_of_levels;
END;
$$ LANGUAGE PLPGSQL VOLATILE;
Then i try to call this function with: select get_levels_test (3), which shows me this error:
ERROR: query has no destination for result data
Hint: If you want to discard the results of a SELECT, use PERFORM instead.
Where: PL/pgSQL function "get_levels_test" line 12 at SQL statement
The list_of_levels table contains just an int column.
In case this is needed, I am using PostgreSQL 8.2.15 (Greenplum Database 4.3.3.1 build 1).
You need to use "return next" which can be used with a composite type or with a table. Please use this feature with caution as you can have performance issues with using this. Do not use this to join the results to another table. Do not return large datasets with this. Only use it for very small results such as a short report.
You also need to add exception handling into your function. I added this to my example function below.
Example table:
create table employees
(id int not null,
manager_id int null,
employee_title text not null)
distributed by (id);
Example data:
insert into employees values
(1, null, 'President'),
(2, 1, 'Vice-President'),
(3, 2, 'Manager'),
(4, 3, 'Engineer'),
(5, null, 'CMO'),
(6, 5, 'Director'),
(7, 6, 'Assistant'),
(8, 7, 'Developer');
Function:
create or replace function get_employees(p_id int) returns setof employees as
$$
declare
v_function_name text := 'get_employees';
v_location int;
v_rec record;
v_rec2 employees;
v_id employees.id%type;
begin
v_location := 1000;
create temporary table t1
(id integer) on commit drop distributed by (id);
v_location := 2000;
for v_rec in (select id from employees where id = p_id and manager_id is null order by id) loop
v_id := v_rec.id;
insert into t1 values (v_id);
while v_id is not null loop
select id into v_id from employees where manager_id = v_id;
if v_id is not null then
insert into t1 values (v_id);
end if;
end loop;
end loop;
v_location := 3000;
for v_rec2 in (select * from employees e join t1 on e.id = t1.id order by e.id) loop
return next v_rec2;
end loop;
exception
when others then
raise exception '(%:%:%)', v_function_name, v_location, sqlerrm;
end;
$$
language plpgsql;
And using the function:
select * From get_employees(1);
id | manager_id | employee_title
----+------------+----------------
1 | | President
2 | 1 | Vice-President
3 | 2 | Manager
4 | 3 | Engineer
(4 rows)
select * From get_employees(5);
id | manager_id | employee_title
----+------------+----------------
5 | | CMO
6 | 5 | Director
7 | 6 | Assistant
8 | 7 | Developer
(4 rows)

MySQL if not exists by two columns update other

I want to insert record if it not exists by 2 columns, and if exists update column 3, here's the query:
INSERT INTO aktai_islaidos_id (akto_id, islaidos_id, savikaina)
SELECT * FROM (SELECT ? as a, ? as b, ? as c) AS tmp
WHERE NOT EXISTS (
SELECT akto_id, islaidos_id, savikaina FROM aktai_islaidos_id
WHERE akto_id = a AND islaidos_id = b
) ON DUPLICATE KEY UPDATE savikaina = VALUES(c);
Now I'm getting error what c not exists in fields list, I understand why, but I didn't know how to complete this query correctly and didn't find any examples like what only where selects duplicate all columns, thanks!
EDIT:
Figured out this with stored procedure:
CREATE PROCEDURE update(
IN akt_id INT,
IN isl_id INT,
IN sav INT)
BEGIN
IF (SELECT count(*) FROM aktai_islaidos_id WHERE akto_id = akt_id AND islaidos_id = isl_id) > 0 THEN
BEGIN
UPDATE aktai_islaidos_id SET savikaina = sav WHERE akto_id = akt_id AND islaidos_id = isl_id;
END;
ELSE
BEGIN
INSERT INTO aktai_islaidos_id (akto_id, islaidos_id, savikaina) VALUES (akt_id, isl_id, sav);
END;
END IF;
END

count number of hierarchical childrens in sql

I have a table that stores parent and left child and right child information. How do i count number of children belongs that parent?
for example my table structure is:
parent left right
--------------------
1 2 3
3 4 5
4 8 9
5 10 11
2 6 7
9 12 null
How do I count number of sub nodes for any parent. For example 4 contains following hierarchical child nodes - 8,9,12 so number of children are 3.
3 contains following sub nodes -> 4,5,10,11,8,9,12 so total number of children 7.
How do I achieve this using SQL query?
create table mytable
( parent int not null,
cleft int null,
cright int null
)
insert into mytable (parent,cleft,cright) values (1,2,3);
insert into mytable (parent,cleft,cright) values (2,6,7);
insert into mytable (parent,cleft,cright) values (3,4,5);
insert into mytable (parent,cleft,cright) values (4,8,9);
insert into mytable (parent,cleft,cright) values (5,10,11);
insert into mytable (parent,cleft,cright) values (6,null,null);
insert into mytable (parent,cleft,cright) values (7,null,null);
insert into mytable (parent,cleft,cright) values (8,13,null);
insert into mytable (parent,cleft,cright) values (9,12,null);
insert into mytable (parent,cleft,cright) values (10,null,null);
insert into mytable (parent,cleft,cright) values (12,null,null);
insert into mytable (parent,cleft,cright) values (13,null,17);
insert into mytable (parent,cleft,cright) values (17,null,null);
DELIMITER $$
CREATE procedure GetChildCount (IN parentID INT)
DETERMINISTIC
BEGIN
declare ch int;
declare this_left int;
declare this_right int;
declare bContinue boolean;
declare count_needs_scan int;
create temporary table asdf999 (node_id int,processed int);
-- insert into asdf999 (node_id,processed) values (1,0);
-- update asdf999 set processed=1;
SET ch = parentID;
set bContinue=true;
while bContinue DO
-- at this point you are sitting at a ch (anywhere in hierarchy)
-- as you are looping and getting/using children
-- save non-null children references: -----------------------------
select cleft into this_left from mytable where parent=ch;
if !isnull(this_left) then
insert asdf999 (node_id,processed) select this_left,0;
end if;
select cright into this_right from mytable where parent=ch;
if !isnull(this_right) then
insert asdf999 (node_id,processed) select this_right,0;
end if;
-- -----------------------------------------------------------------
select count(*) into count_needs_scan from asdf999 where processed=0;
if count_needs_scan=0 then
set bContinue=false;
else
select node_id into ch from asdf999 where processed=0 limit 1;
update asdf999 set processed=1 where node_id=ch;
-- well, it is about to be processed
end if;
END WHILE;
select count(*) as the_count from asdf999;
drop table asdf999;
END $$
DELIMITER ;
call GetChildCount(2); -- answer is 2
call GetChildCount(4); -- answer is 5
I could supply a version that creates a dynamically named table (or temp table) and clobbers it at end if you want . "dynamic sql / prepare statment" inside of a procedure. that way users won't step on each other with shared use of the work table asdf999. so this is not production ready. but the above gives you an idea of the concept