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 edge where edge.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 edge (lot_id, path) (select distinct lot_id, fp.path ||
                                                           subpath(edge.path, index(edge.path, text2ltree(child)))
                                   from edge,
                                        (select path
                                         from edge
                                         where path ~ CAST('*.' || parent AS lquery)) as fp
                                   where edge.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 edge where edge.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 edge_path_unique DEFERRED;

  -- remove everything above the child lot_id in the path
  -- this creates duplicates on path and lot_id
  update edge
  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 edge
  WHERE id IN (SELECT id
               FROM (SELECT id, ROW_NUMBER() OVER (partition BY lot_id, path) AS rnum FROM edge) t
               WHERE t.rnum > 1);

  -- re-activate uniqueness check and perform check
  SET CONSTRAINTS edge_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 edge where lot_id = child_id;
  IF number > 1
  THEN
    delete from edge where path <@ text2ltree(child);
  end if;

END
$$
LANGUAGE plpgsql;



