Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Finding circular references within PostgreSQL table?

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.

like image 368
PL-SQL Developer1000 Avatar asked Nov 11 '22 08:11

PL-SQL Developer1000


2 Answers

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; 
like image 146
Andre Silva Avatar answered Nov 15 '22 06:11

Andre Silva


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
like image 34
klin Avatar answered Nov 15 '22 06:11

klin