This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
devicehub-teal/ereuse_devicehub/resources/lot/dag.sql
2019-12-09 17:24:24 +01:00

113 lines
4.3 KiB
PL/PgSQL

CREATE OR REPLACE FUNCTION add_edge(parent_id uuid, child_id uuid)
/* Adds an edge between ``parent`` and ``child``.
Designed to work with Directed Acyclic Graphs (DAG)
(or said in another way, trees with multiple parents without cycles).
This method will raise an exception if:
- Parent is the same as child.
- Child contains the parent.
- Edge parent - child already exists.
Influenced by:
- https://www.codeproject.com/Articles/22824/A-Model-to-Represent-Directed-Acyclic-Graphs-DAG
- http://patshaughnessy.net/2017/12/12/installing-the-postgres-ltree-extension
- https://en.wikipedia.org/wiki/Directed_acyclic_graph
*/
RETURNS void AS
$$
DECLARE
parent text := replace(CAST(parent_id as text), '-', '_');
child text := replace(CAST(child_id as text), '-', '_');
BEGIN
if parent = child
then
raise exception 'Cannot create edge: the parent is the same as the child.';
end if;
if EXISTS(
SELECT 1
FROM path
where path.path ~ CAST('*.' || child || '.*.' || parent || '.*' as lquery)
)
then
raise exception 'Cannot create edge: child already contains parent.';
end if;
-- We have two subgraphs: the parent subgraph that goes from the parent to the root,
-- and the child subgraph, going from the child (which is the root of this subgraph)
-- to all the leafs.
-- We do the cartesian product from all the paths of the parent subgraph that end in the parent
-- WITH all the paths that start from the child that end to its leafs.
insert into path (lot_id, path) (
select distinct lot_id, fp.path || subpath(path.path, index(path.path, text2ltree(child)))
from path,
(select path.path from path where path.path ~ CAST('*.' || parent AS lquery)) as fp
where path.path ~ CAST('*.' || child || '.*' AS lquery)
);
-- Cleanup: old paths that start with the child (that where used above in the cartesian product)
-- have became a subset of the result of the cartesian product, thus being redundant.
delete from path where path.path ~ CAST(child || '.*' AS lquery);
END
$$
LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_edge(parent_id uuid, child_id uuid)
/* Deletes an edge between ``parent`` and ``child``.
Designed to work with DAG (See ``add_edge`` function).
This method will raise an exception if the relationship does not
exist.
*/
RETURNS void AS
$$
DECLARE
parent text := replace(CAST(parent_id as text), '-', '_');
child text := replace(CAST(child_id as text), '-', '_');
number int;
BEGIN
-- to delete we remove from the path of the descendants of the child
-- (and the child) any ancestor coming from this edge.
-- When we added the edge we did a cartesian product. When removing
-- this part of the path we will have duplicate paths.
-- don't check uniqueness for path key until we delete duplicates
SET CONSTRAINTS path_unique DEFERRED;
-- remove everything above the child lot_id in the path
-- this creates duplicates on path and lot_id
update path
set path = subpath(path, index(path, text2ltree(child)))
where path ~ CAST('*.' || parent || '.' || child || '.*' AS lquery);
-- remove duplicates
-- we need an id field exclusively for this operation
-- from https://wiki.postgresql.org/wiki/Deleting_duplicates
DELETE
FROM path
WHERE id IN (SELECT id
FROM (SELECT id, ROW_NUMBER() OVER (partition BY lot_id, path) AS rnum
FROM path) t
WHERE t.rnum > 1);
-- re-activate uniqueness check and perform check
-- todo we should put this in a kind-of finally clause
SET CONSTRAINTS path_unique IMMEDIATE;
-- After the update the one of the paths of the child will be
-- containing only the child.
-- This can only be when the child has no parent at all.
-- In case the child has more than one parent, remove this path
-- (note that we want it to remove it too from descendants of this
-- child, ex. 'child_id'.'desc1')
select COUNT(1) into number from path where lot_id = child_id;
IF number > 1
THEN
delete from path where path <@ text2ltree(child);
end if;
END
$$
LANGUAGE plpgsql;