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.
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;
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With