Reputation: 133
I have one table with attributes (ID int, SourceID int, TargetID int, TargetType int)
ID SourceID TargetID --------------------- 1 123 456 2 456 789 3 1 123 4 456 1 5 2 1
I want to find out all circular references. I want to write PL/pgsql function for this.
Here circular reference for ID 4 = 456 1 123 456
I want to find such instances. Can anyone suggest me how to proceed for this.
Upvotes: 6
Views: 2813
Reputation: 127
CREATE TABLE members (
id serial PRIMARY KEY,
reports_to_id int REFERENCES members(id) ON DELETE RESTRICT
);
CREATE OR REPLACE FUNCTION circular_reference() RETURNS trigger AS $$
DECLARE circular_ref_exists boolean := false;
BEGIN
IF (NEW.reports_to_id = NEW.id)
THEN
RAISE EXCEPTION 'A self reference has been detected';
END IF;
WITH RECURSIVE cte AS (
SELECT * FROM members
WHERE members.id = NEW.reports_to_id
UNION
SELECT members.* FROM cte
INNER JOIN members ON
members.id = cte.reports_to_id AND
members.id <> NEW.id
)
SELECT true INTO circular_ref_exists FROM cte WHERE reports_to_id = NEW.id;
IF (circular_ref_exists)
THEN
RAISE EXCEPTION 'A circular reference has been detected';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER circular_reference
BEFORE UPDATE ON members
FOR EACH ROW
EXECUTE FUNCTION circular_reference();
Upvotes: 1
Reputation: 4928
This is similar to How to prevent a self-referencing table from becoming circular (SQL-Server version), which poses the question using a didatic example of employee/manager table.
Beyond a function to detect circular references I was interested having a trigger to prevent such data to be inserted or updated in the database.
Here is the code PLpgSQL/PostgreSQL I adapted from mellamokb's answer:
-- example of table structure
CREATE TABLE employee (
ID INTEGER NOT NULL,
ManagerID INTEGER,
CONSTRAINT a PRIMARY KEY (ID),
CONSTRAINT b FOREIGN KEY (ManagerID) REFERENCES employee (ID))
-- function to be used inside trigger
CREATE OR REPLACE FUNCTION CircularReference()
RETURNS TRIGGER
AS
$$
DECLARE _CircularReferenceExists BIT(1) := 0;
BEGIN
WITH RECURSIVE cte AS (
SELECT E.* FROM employee E WHERE E.ID = new.ManagerID
UNION -- union instead of union all to prevent infinite looping.
SELECT E.* FROM employee E JOIN cte C ON C.ManagerID = E.ID AND E.ID <> new.ManagerID
)
SELECT count(*) INTO _CircularReferenceExists FROM cte C WHERE C.ManagerID = new.ManagerID;
IF (SELECT _CircularReferenceExists <> B'0')
THEN
RAISE EXCEPTION 'Circular Reference found: ManagerID = %', new.ManagerID;
END IF;
RETURN NEW;
END
$$ LANGUAGE plpgsql;
-- trigger
CREATE TRIGGER CircularReference
AFTER INSERT OR UPDATE
ON employee
FOR EACH ROW
EXECUTE PROCEDURE CircularReference();
If one already have data in the database just create the trigger and use an UPDATE statement to make it start detecting circular references.
UPDATE employee SET ID = ID;
Upvotes: 4
Reputation: 121634
This can be done with the recursive function below. The function uses intarray extension.
create extension intarray;
The first element of int array arr
is an id
. The rest of the array contains consecutive references source -> target.
If the second and the last elements of the array are equal, then a circular reference was found. (1)
We must look for inner circular references and eliminate them (or we'll finish with stack overflow). (2)
create or replace function find_cref(arr int[])
returns setof int[] language plpgsql
as $$
declare
vlen int = #arr;
vtarget int;
begin
if arr[2] = arr[vlen] then -- (1)
return query select arr;
else
if #uniq(sort(subarray(arr, 2)))+ 1 = vlen then -- (2)
for vtarget in
select target from the_table where source = arr[vlen]
loop
return query select find_cref (arr+ vtarget);
end loop;
end if;
end if;
end $$;
select c[1] id, subarray(c, 2) cref
from (
select find_cref(array[id, source, target]) c
from the_table) x
Upvotes: 1