diff --git a/src/Data/Graph/Inductive/Query/Cycle.hs b/src/Data/Graph/Inductive/Query/Cycle.hs index 42a5760..326df4d 100644 --- a/src/Data/Graph/Inductive/Query/Cycle.hs +++ b/src/Data/Graph/Inductive/Query/Cycle.hs @@ -19,9 +19,9 @@ -- -- 1. An optional direction parameter, specifying which nodes to visit next. -- --- [@x@] undirectional: ignore edge direction +-- [@u@] undirectional: ignore edge direction -- [@r@] reversed: walk edges in reverse --- [@x@] user defined: speciy which paths to follow +-- [@x@] user defined: specify which paths to follow -- -- 2. Base name. -- diff --git a/src/Data/Graph/Inductive/Query/Path.hs b/src/Data/Graph/Inductive/Query/Path.hs index 78911fd..9df7bf3 100644 --- a/src/Data/Graph/Inductive/Query/Path.hs +++ b/src/Data/Graph/Inductive/Query/Path.hs @@ -15,7 +15,7 @@ -- | Detecting existence of paths in graphs, and finding the paths. -- --- Some path related functions already exist in @fgl@ in the Query modules on +-- Some path related functions already exist in @fgl@ in the Query modules of -- the algorithms they're based on. In this module I'm putting additional path -- related utilities I need. module Data.Graph.Inductive.Query.Path diff --git a/src/Database/Persist/Local/Sql.hs b/src/Database/Persist/Local/Sql.hs index 37634fe..9282ad6 100644 --- a/src/Database/Persist/Local/Sql.hs +++ b/src/Database/Persist/Local/Sql.hs @@ -16,7 +16,62 @@ module Database.Persist.Local.Sql ( dummyFromField , rawSqlWithGraph - , containsCycle + , FollowDirection (..) + -- * Checking for cycle existence + -- $cyclic + -- ** Standard + , cyclic + , cyclicn + , xcyclic + , xcyclicn + -- ** Undirected + , ucyclic + , ucyclicn + -- ** Reversed + , rcyclic + , rcyclicn + -- * Checking for reachability, i.e. existence of path + -- $connects + -- ** Standard + , connects + , mconnects + , connectsm + , mconnectsm + , xconnects + , xmconnects + , xconnectsm + , xmconnectsm + -- ** Undirected + , uconnects + , umconnects + , uconnectsm + , umconnectsm + -- ** Reversed + , rconnects + , rmconnects + , rconnectsm + , rmconnectsm + -- * Finding paths + -- $path + -- ** Standard + , path + , mpath + , pathm + , mpathm + , xpath + , xmpath + , xpathm + , xmpathm + -- ** Undirected + , upath + , umpath + , upathm + , umpathm + -- ** Reversed + , rpath + , rmpath + , rpathm + , rmpathm ) where @@ -31,7 +86,7 @@ import Database.Persist import Database.Persist.Sql import Database.Persist.Sql.Util -import qualified Data.Text as T (intercalate) +import qualified Data.Text as T (null, intercalate) import Database.Persist.Local.Class.PersistEntityGraph import Database.Persist.Local.Class.PersistQueryForest @@ -61,6 +116,9 @@ sourceFieldFromProxy -> EntityField edge (Key node) sourceFieldFromProxy _ = sourceField +data FollowDirection = FollowForward | FollowBackward | FollowBoth + deriving (Eq, Show) + rawSqlWithGraph :: ( RawSql a , MonadIO m @@ -125,139 +183,9 @@ rawSqlWithGraph dir root parent child sub vals = do vals' = toPersistValue root : vals rawSql sql vals' -containsCycle' - :: ( MonadIO m - , PersistEntity node - , PersistEntity edge - , PersistEntityGraph node edge - , SqlBackend ~ PersistEntityBackend node - , SqlBackend ~ PersistEntityBackend edge - ) - => Proxy (node, edge) - -> ReaderT SqlBackend m [Single Int] -containsCycle' proxy = do - conn <- ask - let tNode = entityDef $ dummyFromFst proxy - tEdge = entityDef $ dummyFromSnd proxy - fwd = destFieldFromProxy proxy - bwd = sourceFieldFromProxy proxy - start = DBName "temp_start_cte" - temp = DBName "temp_hierarchy_cte" - tid = DBName "id" - tpath = DBName "path" - tcycle = DBName "cycle" - dbname = connEscapeName conn - sql = mconcat - [ "WITH RECURSIVE " - , dbname start - , " (" - , T.intercalate "," $ map dbname [tid, tpath, tcycle] - , ") AS ( SELECT " - , dbname $ entityDB tNode - , "." - , dbname $ fieldDB $ entityId tNode - - , ", " - - , "ARRAY[" - , dbname $ entityDB tNode - , "." - , dbname $ fieldDB $ entityId tNode - , "]" - - , ", " - - , "FALSE" - - , " FROM " - , dbname $ entityDB tNode - , " LEFT OUTER JOIN " - , dbname $ entityDB tEdge - , " ON " - - , dbname $ entityDB tNode - , "." - , dbname $ fieldDB $ entityId tNode - - , " = " - - , dbname $ entityDB tEdge - , "." - , dbname $ fieldDB $ persistFieldDef fwd - - , " WHERE " - , dbname $ entityDB tEdge - , "." - , dbname $ fieldDB $ persistFieldDef fwd - , " IS NULL " - , " ), " - , dbname temp - , " (" - , T.intercalate "," $ map dbname [tid, tpath, tcycle] - , ") AS ( SELECT " - , "* FROM " - , dbname start - - , " UNION ALL SELECT " - , dbname $ entityDB tEdge - , "." - , dbname $ fieldDB $ persistFieldDef fwd - - , ", " - - , dbname temp - , "." - , dbname tpath - , " || " - , dbname $ entityDB tEdge - , "." - , dbname $ fieldDB $ persistFieldDef fwd - - , ", " - - , dbname $ entityDB tEdge - , "." - , dbname $ fieldDB $ persistFieldDef fwd - , " = " - , "ANY(", dbname temp, ".", dbname tpath, ")" - - , " FROM " - , dbname $ entityDB tEdge - , " INNER JOIN " - , dbname temp - , " ON " - - , dbname $ entityDB tEdge - , "." - , dbname $ fieldDB $ persistFieldDef bwd - - , " = " - - , dbname temp, ".", dbname tid - - , " WHERE NOT " - , dbname temp, ".", dbname tcycle - , " ) " - - , "(" - , "SELECT 1 FROM " - , dbname start - - , " UNION ALL " - - , "SELECT 1 FROM " - , dbname temp - , " WHERE ", dbname tcycle, " = TRUE" - , ") LIMIT 1" - ] - rawSql sql [] - --- | Check whether the graph contains (directed) cycles. --- --- Start with nodes which don't have in-edges, and traverse through the edges, --- either until we visit all the nodes, or until we find a node we visited --- before. If we can't find nodes without in-edges, or we found a node we --- visited before, then a cycle exists. +-- | The actual SQL query for checking for cycles. It's a bit hard to figure +-- out the structure of the query from the code, so here's what it more-or-less +-- looks like, to help navigate the code: -- -- > WITH RECURSIVE -- > start (id, path, cycle) AS ( @@ -277,26 +205,955 @@ containsCycle' proxy = do -- > WHERE NOT temp.cycle -- > ) -- > ( SELECT 1 --- > FROM start +-- > FROM node LEFT OUTER JOIN temp +-- > ON node.id = temp.id +-- > WHERE temp.id IS NULL -- > UNION ALL -- > SELECT 1 -- > FROM temp -- > WHERE cycle = true -- > ) -- > LIMIT 1 --- --- The parent and child fields are interchangeable, which is an opportunity to --- optimize. Currently the recursion goes from parents to children (i.e. --- towards decendants), but it could be changed, or made available for the user --- to choose, if benchmarks reveal performace differences. -containsCycle +xcyclicn' + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => FollowDirection + -> [Filter edge] + -> Maybe [Key node] + -> Proxy (node, edge) + -> ReaderT SqlBackend m [Single Int] +xcyclicn' follow filter minitials proxy = do + conn <- ask + let tNode = entityDef $ dummyFromFst proxy + tEdge = entityDef $ dummyFromSnd proxy + fwd = persistFieldDef $ destFieldFromProxy proxy + bwd = persistFieldDef $ sourceFieldFromProxy proxy + start = DBName "temp_start_cte" + temp = DBName "temp_hierarchy_cte" + tid = DBName "id" + tpath = DBName "path" + tcycle = DBName "cycle" + dbname = connEscapeName conn + t ^* f = dbname t <> "." <> dbname f + t <#> s = dbname t <> " INNER JOIN " <> dbname s + t <# s = dbname t <> " LEFT OUTER JOIN " <> dbname s + + sqlStartFrom forward = mconcat + [ " FROM ", entityDB tNode <# entityDB tEdge + , " ON " + , entityDB tNode ^* fieldDB (entityId tNode) + , " = " + , entityDB tEdge ^* fieldDB forward + + , " WHERE " + , entityDB tEdge ^* fieldDB forward + , " IS NULL" + ] + + sqlStart = mconcat + [ "SELECT " + , entityDB tNode ^* fieldDB (entityId tNode), ", " + , "ARRAY[", entityDB tNode ^* fieldDB (entityId tNode), "], " + , "FALSE" + , case minitials of + Nothing -> case follow of + FollowForward -> sqlStartFrom fwd + FollowBackward -> sqlStartFrom bwd + FollowBoth -> " FROM " <> dbname (entityDB tNode) + Just initials -> mconcat + [ " FROM ", dbname $ entityDB tNode + , " WHERE ", entityDB tNode ^* fieldDB (entityId tNode) + , " IN ?" + ] + ] + + filt = filterClause False conn filter + fvals = getFiltsValues conn filter + sqlStep forward backward = mconcat + [ "SELECT " + , entityDB tEdge ^* fieldDB forward, ", " + , temp ^* tpath, " || ", entityDB tEdge ^* fieldDB forward, ", " + , entityDB tEdge ^* fieldDB forward, " = ANY(", temp ^* tpath, ")" + , " FROM ", entityDB tEdge <#> temp + , " ON ", entityDB tEdge ^* fieldDB backward, " = ", temp ^* tid + , if T.null filt + then " WHERE NOT " <> temp ^* tcycle + else filt <> " AND NOT " <> temp ^* tcycle + ] + + sqlCycles = mconcat + [ "SELECT 1 FROM " + , dbname temp + , " WHERE ", dbname tcycle, " = TRUE" + ] + + sql = mconcat + [ "WITH RECURSIVE " + , dbname start + , " (" + , T.intercalate "," $ map dbname [tid, tpath, tcycle] + , ") AS ( " + , sqlStart + , " ), " + , dbname temp + , " (" + , T.intercalate "," $ map dbname [tid, tpath, tcycle] + , ") AS ( SELECT " + , "* FROM " + , dbname start + + , " UNION ALL " + , case follow of + FollowForward -> sqlStep fwd bwd + FollowBackward -> sqlStep bwd fwd + FollowBoth -> mconcat + [ "(" + , sqlStep fwd bwd + , " UNION ALL " + , sqlStep bwd fwd + , ")" + ] + , " ) " + , case follow of + FollowBoth -> sqlCycles <> " LIMIT 1" + _ -> case minitials of + Just _ -> sqlCycles <> " LIMIT 1" + Nothing -> mconcat + [ "(", sqlCycles, " UNION ALL " + , "SELECT 1" + , " FROM ", entityDB tNode <# temp + , " ON " + , entityDB tNode ^* fieldDB (entityId tNode) + , " = " + , temp ^* tid + , " WHERE ", temp ^* tid, " IS NULL" + , ") LIMIT 1" + ] + ] + msval = PersistList . map toPersistValue <$> minitials + vals = maybe id (:) msval fvals + rawSql sql vals + +-- $cyclic +-- Testing for and detecting cycles in graphs. +-- +-- Names consist of: +-- +-- 1. An optional direction parameter, specifying which nodes to visit next. +-- +-- [@u@] undirectional: ignore edge direction +-- [@r@] reversed: walk edges in reverse +-- [@x@] user defined: specify which paths to follow +-- +-- 2. Base name. +-- +-- [@cyclic@] checks for existence of cycles +-- [@cycles@] returns the cyclic paths, if any exist +-- +-- 3. An optional @n@, in which case a user-given subset of the graph's nodes +-- will be visited, instead of visiting /all/ the nodes. + +cyclic :: ( MonadIO m - , PersistEntity node - , PersistEntity edge , PersistEntityGraph node edge , SqlBackend ~ PersistEntityBackend node , SqlBackend ~ PersistEntityBackend edge ) => Proxy (node, edge) -> ReaderT SqlBackend m Bool -containsCycle = fmap (not . null) . containsCycle' +cyclic = xcyclic FollowForward [] + +cyclicn + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => [Key node] + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +cyclicn = xcyclicn FollowForward [] + +xcyclic + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => FollowDirection + -> [Filter edge] + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +xcyclic fw flt = fmap (not . null) . xcyclicn' fw flt Nothing + +xcyclicn + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => FollowDirection + -> [Filter edge] + -> [Key node] + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +xcyclicn fw flt ns = fmap (not . null) . xcyclicn' fw flt (Just ns) + +ucyclic + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Proxy (node, edge) + -> ReaderT SqlBackend m Bool +ucyclic = xcyclic FollowBoth [] + +ucyclicn + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => [Key node] + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +ucyclicn = xcyclicn FollowBoth [] + +rcyclic + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Proxy (node, edge) + -> ReaderT SqlBackend m Bool +rcyclic = xcyclic FollowBackward [] + +rcyclicn + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => [Key node] + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +rcyclicn = xcyclicn FollowBackward [] + +-- $connects +-- Testing for existence of paths. +-- +-- Names consist of: +-- +-- 1. An optional direction parameter, specifying which nodes to visit next. +-- +-- [(none)] forward: follow edge direction +-- [@u@] undirectional: ignore edge direction +-- [@r@] reversed: walk edges in reverse +-- [@x@] user defined: specify which paths to follow +-- +-- 2. An optional source node parameter, specifying from which nodes to start +-- the search. +-- +-- [(none)] one: start with a single specified node +-- [@m@] multi: start with a given list of nodes, or /all/ nodes +-- +-- 3. Base name: @connects@. +-- +-- 4. An optional destination node parameter, specifying which paths to pick +-- based on their destination nodes. +-- +-- [(none)] one: start with a single specified node +-- [@m@] multi: start with a given list of nodes, or /all/ nodes + +(?:) :: Maybe a -> [a] -> [a] +(?:) = maybe id (:) +infixr 5 ?: + +-- | It more-or-less looks like this: +-- +-- > WITH RECURSIVE +-- > temp (id, path, cycle) AS ( +-- > SELECT 3, ARRAY[3], false +-- > UNION ALL +-- > SELECT edge.parent, +-- > temp.path || edge.parent, +-- > edge.parent = ANY(temp.path) +-- > FROM edge INNER JOIN temp +-- > ON edge.child = temp.id +-- > WHERE NOT temp.cycle +-- > ) +-- > SELECT 1 WHERE EXISTS ( +-- > SELECT path +-- > FROM temp +-- > WHERE id = 8 +-- > ) +xmconnectsm' + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => FollowDirection + -> [Filter edge] + -> Maybe [Key node] + -> Maybe [Key node] + -> Maybe Int -- filter on path length max + -> Proxy (node, edge) + -> ReaderT SqlBackend m [Single Int] +xmconnectsm' follow filter msource mdest mlen proxy = do + conn <- ask + let tNode = entityDef $ dummyFromFst proxy + tEdge = entityDef $ dummyFromSnd proxy + fwd = persistFieldDef $ destFieldFromProxy proxy + bwd = persistFieldDef $ sourceFieldFromProxy proxy + temp = DBName "temp_hierarchy_cte" + tid = DBName "id" + tpath = DBName "path" + tcycle = DBName "cycle" + dbname = connEscapeName conn + t ^* f = dbname t <> "." <> dbname f + t <#> s = dbname t <> " INNER JOIN " <> dbname s + t <# s = dbname t <> " LEFT OUTER JOIN " <> dbname s + + filt = filterClause False conn filter + fvals = getFiltsValues conn filter + sqlStep forward backward = mconcat + [ "SELECT " + , entityDB tEdge ^* fieldDB forward, ", " + , temp ^* tpath, " || ", entityDB tEdge ^* fieldDB forward, ", " + , entityDB tEdge ^* fieldDB forward, " = ANY(", temp ^* tpath, ")" + , " FROM ", entityDB tEdge <#> temp + , " ON ", entityDB tEdge ^* fieldDB backward, " = ", temp ^* tid + , if T.null filt + then " WHERE NOT " <> temp ^* tcycle + else filt <> " AND NOT " <> temp ^* tcycle + ] + + sql = mconcat + [ "WITH RECURSIVE " + , dbname temp + , " (" + , T.intercalate "," $ map dbname [tid, tpath, tcycle] + , ") AS ( SELECT " + , entityDB tNode ^* fieldDB (entityId tNode), ", " + , "ARRAY[", entityDB tNode ^* fieldDB (entityId tNode), "], " + , "FALSE" + , case msource of + Nothing -> " FROM " <> dbname (entityDB tNode) + Just _ -> mconcat + [ " FROM ", dbname $ entityDB tNode + , " WHERE ", entityDB tNode ^* fieldDB (entityId tNode) + , " IN ?" + ] + , " UNION ALL " + , case follow of + FollowForward -> sqlStep fwd bwd + FollowBackward -> sqlStep bwd fwd + FollowBoth -> mconcat + [ "(" + , sqlStep fwd bwd + , " UNION ALL " + , sqlStep bwd fwd + , ")" + ] + , " ) SELECT 1 WHERE EXISTS ( SELECT ", temp ^* tpath + , " FROM ", dbname temp + , case mdest of + Nothing -> "" + Just _ -> " WHERE ", temp ^* tid, " IN ?" + , case mlen of + Nothing -> "" + Just _ -> " AND array_length(", temp ^* tpath, ", 1) <= ?" + , " )" + ] + toP = fmap toPersistValue + toPL = fmap $ PersistList . map toPersistValue + vals = toPL msource ?: fvals ++ toPL mdest ?: toP mlen ?: [] + rawSql sql vals + +connects + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Key node + -> Key node + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +connects = xconnects FollowForward [] + +mconnects + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Maybe [Key node] + -> Key node + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +mconnects = xmconnects FollowForward [] + +connectsm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Key node + -> Maybe [Key node] + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +connectsm = xconnectsm FollowForward [] + +mconnectsm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Maybe [Key node] + -> Maybe [Key node] + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +mconnectsm = xmconnectsm FollowForward [] + +xconnects + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => FollowDirection + -> [Filter edge] + -> Key node + -> Key node + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +xconnects fw flt src dest = xmconnectsm fw flt (Just [src]) (Just [dest]) + +xmconnects + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => FollowDirection + -> [Filter edge] + -> Maybe [Key node] + -> Key node + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +xmconnects fw flt msrc dest = xmconnectsm fw flt msrc (Just [dest]) + +xconnectsm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => FollowDirection + -> [Filter edge] + -> Key node + -> Maybe [Key node] + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +xconnectsm fw flt src = xmconnectsm fw flt (Just [src]) + +xmconnectsm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => FollowDirection + -> [Filter edge] + -> Maybe [Key node] + -> Maybe [Key node] + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +xmconnectsm fw flt msrc mdest mlen p = + not . null <$> xmconnectsm' fw flt msrc mdest mlen p + +uconnects + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Key node + -> Key node + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +uconnects = xconnects FollowBoth [] + +umconnects + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Maybe [Key node] + -> Key node + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +umconnects = xmconnects FollowBoth [] + +uconnectsm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Key node + -> Maybe [Key node] + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +uconnectsm = xconnectsm FollowBoth [] + +umconnectsm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Maybe [Key node] + -> Maybe [Key node] + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +umconnectsm = xmconnectsm FollowBoth [] + +rconnects + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Key node + -> Key node + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +rconnects = xconnects FollowBackward [] + +rmconnects + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Maybe [Key node] + -> Key node + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +rmconnects = xmconnects FollowBackward [] + +rconnectsm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Key node + -> Maybe [Key node] + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +rconnectsm = xconnectsm FollowBackward [] + +rmconnectsm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Maybe [Key node] + -> Maybe [Key node] + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m Bool +rmconnectsm = xmconnectsm FollowBackward [] + +-- $path +-- Findings paths between graph nodes. +-- +-- Names consist of: +-- +-- 1. An optional direction parameter, specifying which nodes to visit next. +-- +-- [(none)] forward: follow edge direction +-- [@u@] undirectional: ignore edge direction +-- [@r@] reversed: walk edges in reverse +-- [@x@] user defined: specify which paths to follow +-- +-- 2. An optional source node parameter, specifying from which nodes to start +-- the search. +-- +-- [(none)] one: start with a single specified node +-- [@m@] multi: start with a given list of nodes, or /all/ nodes +-- +-- 3. Base name: @path@. +-- +-- 4. An optional destination node parameter, specifying which paths to pick +-- based on their destination nodes. +-- +-- [(none)] one: start with a single specified node +-- [@m@] multi: start with a given list of nodes, or /all/ nodes + +-- | It more-or-less looks like this: +-- +-- > WITH RECURSIVE +-- > temp (id, path, cycle) AS ( +-- > SELECT 3, ARRAY[3], false +-- > UNION ALL +-- > SELECT edge.parent, +-- > temp.path || edge.parent, +-- > edge.parent = ANY(temp.path) +-- > FROM edge INNER JOIN temp +-- > ON edge.child = temp.id +-- > WHERE NOT temp.cycle +-- > ) +-- > SELECT path +-- > FROM temp +-- > WHERE id = 8 +-- > ORDER BY array_length(path, 1) +xmpathm' + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => FollowDirection + -> [Filter edge] + -> Maybe [Key node] + -> Maybe [Key node] + -> Maybe Int -- filter on path length max + -> Maybe Int -- limit number of results + -> Proxy (node, edge) + -> ReaderT SqlBackend m [Single [Key node]] +xmpathm' follow filter msource mdest mlen mlim proxy = do + conn <- ask + let tNode = entityDef $ dummyFromFst proxy + tEdge = entityDef $ dummyFromSnd proxy + fwd = persistFieldDef $ destFieldFromProxy proxy + bwd = persistFieldDef $ sourceFieldFromProxy proxy + temp = DBName "temp_hierarchy_cte" + tid = DBName "id" + tpath = DBName "path" + tcycle = DBName "cycle" + dbname = connEscapeName conn + t ^* f = dbname t <> "." <> dbname f + t <#> s = dbname t <> " INNER JOIN " <> dbname s + t <# s = dbname t <> " LEFT OUTER JOIN " <> dbname s + + filt = filterClause False conn filter + fvals = getFiltsValues conn filter + sqlStep forward backward = mconcat + [ "SELECT " + , entityDB tEdge ^* fieldDB forward, ", " + , temp ^* tpath, " || ", entityDB tEdge ^* fieldDB forward, ", " + , entityDB tEdge ^* fieldDB forward, " = ANY(", temp ^* tpath, ")" + , " FROM ", entityDB tEdge <#> temp + , " ON ", entityDB tEdge ^* fieldDB backward, " = ", temp ^* tid + , if T.null filt + then " WHERE NOT " <> temp ^* tcycle + else filt <> " AND NOT " <> temp ^* tcycle + ] + + sql = mconcat + [ "WITH RECURSIVE " + , dbname temp + , " (" + , T.intercalate "," $ map dbname [tid, tpath, tcycle] + , ") AS ( SELECT " + , entityDB tNode ^* fieldDB (entityId tNode), ", " + , "ARRAY[", entityDB tNode ^* fieldDB (entityId tNode), "], " + , "FALSE" + , case msource of + Nothing -> " FROM " <> dbname (entityDB tNode) + Just _ -> mconcat + [ " FROM ", dbname $ entityDB tNode + , " WHERE ", entityDB tNode ^* fieldDB (entityId tNode) + , " IN ?" + ] + , " UNION ALL " + , case follow of + FollowForward -> sqlStep fwd bwd + FollowBackward -> sqlStep bwd fwd + FollowBoth -> mconcat + [ "(" + , sqlStep fwd bwd + , " UNION ALL " + , sqlStep bwd fwd + , ")" + ] + , " ) SELECT ", temp ^* tpath + , " FROM ", dbname temp + , case mdest of + Nothing -> "" + Just _ -> " WHERE ", temp ^* tid, " IN ?" + , case mlen of + Nothing -> "" + Just _ -> " AND array_length(", temp ^* tpath, ", 1) <= ?" + , " ORDER BY array_length(", temp ^* tpath, ", 1)" + , case mlim of + Nothing -> "" + Just _ -> " LIMIT ?" + ] + toP = fmap toPersistValue + toPL = fmap $ PersistList . map toPersistValue + vals = + toPL msource ?: fvals ++ toPL mdest ?: toP mlen ?: toP mlim ?: [] + rawSql sql vals + +path + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Key node + -> Key node + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +path = xpath FollowForward [] + +mpath + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Maybe [Key node] + -> Key node + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +mpath = xmpath FollowForward [] + +pathm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Key node + -> Maybe [Key node] + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +pathm = xpathm FollowForward [] + +mpathm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Maybe [Key node] + -> Maybe [Key node] + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +mpathm = xmpathm FollowForward [] + +xpath + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => FollowDirection + -> [Filter edge] + -> Key node + -> Key node + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +xpath fw flt src dest = xmpathm fw flt (Just [src]) (Just [dest]) + +xmpath + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => FollowDirection + -> [Filter edge] + -> Maybe [Key node] + -> Key node + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +xmpath fw flt msrc dest = xmpathm fw flt msrc (Just [dest]) + +xpathm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => FollowDirection + -> [Filter edge] + -> Key node + -> Maybe [Key node] + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +xpathm fw flt src = xmpathm fw flt (Just [src]) + +xmpathm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => FollowDirection + -> [Filter edge] + -> Maybe [Key node] + -> Maybe [Key node] + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +xmpathm fw flt msrc mdest mlen mlim p = + map unSingle <$> xmpathm' fw flt msrc mdest mlen mlim p + +upath + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Key node + -> Key node + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +upath = xpath FollowBoth [] + +umpath + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Maybe [Key node] + -> Key node + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +umpath = xmpath FollowBoth [] + +upathm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Key node + -> Maybe [Key node] + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +upathm = xpathm FollowBoth [] + +umpathm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Maybe [Key node] + -> Maybe [Key node] + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +umpathm = xmpathm FollowBoth [] + +rpath + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Key node + -> Key node + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +rpath = xpath FollowBackward [] + +rmpath + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Maybe [Key node] + -> Key node + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +rmpath = xmpath FollowBackward [] + +rpathm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Key node + -> Maybe [Key node] + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +rpathm = xpathm FollowBackward [] + +rmpathm + :: ( MonadIO m + , PersistEntityGraph node edge + , SqlBackend ~ PersistEntityBackend node + , SqlBackend ~ PersistEntityBackend edge + ) + => Maybe [Key node] + -> Maybe [Key node] + -> Maybe Int + -> Maybe Int + -> Proxy (node, edge) + -> ReaderT SqlBackend m [[Key node]] +rmpathm = xmpathm FollowBackward []