Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent and/or detect cycles in postgres

Assuming a schema like the following:

CREATE TABLE node (
  id       SERIAL PRIMARY KEY,
  name     VARCHAR,
  parentid INT REFERENCES node(id)
);

Further, let's assume the following data is present:

INSERT INTO node (name,parentid) VALUES
('A',NULL),
('B',1),
('C',1);

Is there a way to prevent cycles from being created? Example:

UPDATE node SET parentid = 2 WHERE id = 1;

This would create a cycle of 1->2->1->...

like image 751
sschober Avatar asked Oct 31 '14 09:10

sschober


People also ask

Can we use for loop in PostgreSQL?

The PostgreSQL For Loop. Postgresql supports For loop statements to iterate through a range of integers or results from a sequence query. The For loop is used to iterate over a set of numbers or objects.

Can procedure return a value in PostgreSQL?

In case you want to return a value from a stored procedure, you can use output parameters. The final values of the output parameters will be returned to the caller.

Do while loops PostgreSQL?

The PostgreSQL WHILE LOOP evaluates the condition defined to decide whether the loop should be terminated or continued for execution. If the condition defined with PostgreSQL WHILE LOOP evaluates to true, then the body of WHILE LOOP or code statements are written inside the PostgreSQL WHILE LOOP is executed.

What is return query in Postgres?

RETURN QUERY appends the results of executing a query to the function's result set. RETURN NEXT and RETURN QUERY can be freely intermixed in a single set-returning function, in which case their results will be concatenated.


2 Answers

Your trigger simplified and optimized, should be considerably faster:

CREATE OR REPLACE FUNCTION detect_cycle()
  RETURNS TRIGGER
  LANGUAGE plpgsql AS
$func$
BEGIN
   IF EXISTS (
      WITH RECURSIVE search_graph(parentid, path, cycle) AS ( -- relevant columns
          -- check ahead, makes 1 step less
         SELECT g.parentid, ARRAY[g.id, g.parentid], (g.id = g.parentid)
         FROM   node g
         WHERE  g.id = NEW.id  -- only test starting from new row
         
         UNION ALL
         SELECT g.parentid, sg.path || g.parentid, g.parentid = ANY(sg.path)
         FROM   search_graph sg
         JOIN   node g ON g.id = sg.parentid
         WHERE  NOT sg.cycle
         )
      SELECT FROM search_graph
      WHERE  cycle
      LIMIT  1  -- stop evaluation at first find
      )
   THEN
      RAISE EXCEPTION 'Loop detected!';
   ELSE
     RETURN NEW;
   END IF;
END
$func$;

You don't need dynamic SQL, you don't need to count, you don't need all the columns and you don't need to test the whole table for every single row.

CREATE TRIGGER detect_cycle_after_update
AFTER INSERT OR UPDATE ON node
FOR EACH ROW EXECUTE PROCEDURE detect_cycle();

An INSERT like this has to be prohibited, too:

INSERT INTO node (id, name,parentid) VALUES (8,'D',9), (9,'E',8);
like image 85
Erwin Brandstetter Avatar answered Oct 05 '22 01:10

Erwin Brandstetter


To answer my own question, I came up with a trigger that prevents this:

CREATE OR REPLACE FUNCTION detect_cycle() RETURNS TRIGGER AS
$func$
DECLARE
  loops INTEGER;
BEGIN
   EXECUTE 'WITH RECURSIVE search_graph(id, parentid, name, depth, path, cycle) AS (
        SELECT g.id, g.parentid, g.name, 1,
          ARRAY[g.id],
          false
        FROM node g
      UNION ALL
        SELECT g.id, g.parentid, g.name, sg.depth + 1,
          path || g.id,
          g.id = ANY(path)
        FROM node g, search_graph sg
        WHERE g.id = sg.parentid AND NOT cycle
)
SELECT count(*) FROM search_graph where cycle = TRUE' INTO loops;
IF loops > 0 THEN
  RAISE EXCEPTION 'Loop detected!';
ELSE
  RETURN NEW;
END IF;
END
$func$ LANGUAGE plpgsql;

CREATE TRIGGER detect_cycle_after_update
AFTER UPDATE ON node
FOR EACH ROW EXECUTE PROCEDURE detect_cycle();

So, if you try to create a loop, like in the question:

UPDATE node SET parentid = 2 WHERE id = 1;

You get an EXCEPTION:

ERROR:  Loop detected!
like image 42
sschober Avatar answered Oct 05 '22 00:10

sschober