From fec4376b8053add55139b252ecdb9911a7868681 Mon Sep 17 00:00:00 2001 From: Sofya Date: Fri, 25 Jan 2019 09:09:59 +0300 Subject: [PATCH] version 0.0.0.15: upd get and put on graphs (#22) * put updated * 0.0.0.15: upd get in graphs * example added --- CHANGELOG.md | 4 + example/Main.hs | 131 +++++++++++++ hasbolt-extras.cabal | 17 +- .../Extras/Graph/Internal/AbstractGraph.hs | 20 +- .../Bolt/Extras/Graph/Internal/Class.hs | 5 +- .../Bolt/Extras/Graph/Internal/Get.hs | 156 +++++++++------ .../Bolt/Extras/Graph/Internal/GraphQuery.hs | 77 +++++--- .../Bolt/Extras/Graph/Internal/Put.hs | 179 +++++++++--------- 8 files changed, 416 insertions(+), 173 deletions(-) create mode 100644 example/Main.hs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a6971b..3c33595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +## [0.0.0.15] - 2019-01-22 +### Changed +- Ability to choose whether to return entity or not in graphs. + ## [0.0.0.14] - 2018-12-25 ### Added - `mergeGraphs`, ability to take not all node properties from DB. diff --git a/example/Main.hs b/example/Main.hs new file mode 100644 index 0000000..c7dd955 --- /dev/null +++ b/example/Main.hs @@ -0,0 +1,131 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} + +module Main where + +import Control.Exception (bracket) +import Control.Monad.State (execState, modify) +import Data.Aeson (encode) +import qualified Data.ByteString.Lazy.Char8 as B (putStrLn) +import Data.Default (def) +import Data.Map.Strict ((!)) +import Data.Text (Text) +import Database.Bolt (BoltActionT, BoltCfg (..), + Value (..), close, connect, run) +import Database.Bolt.Extras (NodeLike (..), + URelationLike (..)) +import Database.Bolt.Extras.Graph +import Database.Bolt.Extras.Template (makeNodeLike, makeURelationLike) +import GHC.Generics (Generic) + +-- | Configuration for connection to local database. +boltCfg :: BoltCfg +boltCfg = def { host = "localhost" + , user = "neo4j" + , password = "12345" + } + +-- | Helper to run queries in Neo4j DB. +-- +runQueryDB :: BoltActionT IO a -> IO a +runQueryDB act = bracket (connect boltCfg) close (`run` act) + +data ExampleNode = ExampleNode { exampleFieldT :: Text + , exampleFieldI :: Int + } + deriving (Show, Generic) + +data EXAMPLE_RELATION = EXAMPLE_RELATION + deriving (Show, Generic) + +makeNodeLike ''ExampleNode +makeURelationLike ''EXAMPLE_RELATION + +exNodeAVar :: Text +exNodeAVar = "nodeA" + +exNodeBVar :: Text +exNodeBVar = "nodeB" + +-- | Builds query: +-- CREATE (nodeA:ExampleNode { exampleFieldT: "A" , exampleFieldI: 1}) WITH nodeA +-- MERGE (nodeB:ExampleNode { exampleFieldT: "B" , exampleFieldI: 2}) WITH nodeB +-- MERGE (nodeA)-[nodeA0nodeB:EXAMPLE_RELATION]-(nodeB) WITH nodeA, nodeB, nodeA0nodeB +-- RETURN ID(nodeA), ID(nodeB), ID(nodeA0nodeB) +-- +examplePutGraph :: GraphPutRequest +examplePutGraph = flip execState emptyGraph $ do + modify $ addNode exNodeAVar (CreateN . toNode $ exNodeA) + modify $ addNode exNodeBVar (MergeN . toNode $ exNodeB) + + modify $ addRelation exNodeAVar exNodeBVar (MergeR . toURelation $ exRel) + where + exNodeA = ExampleNode "A" 1 + exNodeB = ExampleNode "B" 2 + exRel = EXAMPLE_RELATION + +-- | Builds query: +-- MATCH (nodeA)-[:EXAMPLE_RELATION]-(nodeB), +-- (nodeA:ExampleNode { exampleField: "A" }), +-- (nodeB:ExampleNode { exampleField: "B" }) +-- RETURN { id: ID(nodeA), labels: labels(nodeA), props: properties(nodeA) } +-- +exampleGetGraphB :: GraphGetRequest +exampleGetGraphB = flip execState emptyGraph $ do + modify $ addNode exNodeAVar exNodeA + modify $ addNode exNodeBVar exNodeB + + modify $ addRelation exNodeAVar exNodeBVar exRel + where + exNodeA = defaultNodeReturn # withLabelQ ''ExampleNode + # withProp ("exampleFieldT", T "A") + # withReturn allProps + + exNodeB = defaultNodeNotReturn # withLabelQ ''ExampleNode + # withProp ("exampleFieldT", T "B") + + exRel = defaultRelNotReturn # withLabelQ ''EXAMPLE_RELATION + +-- | Builds query: +-- MATCH (nodeA:ExampleNode { exampleField: "A" }) +-- RETURN { id: ID(nodeA), labels: labels(nodeA), props: nodeA {.exampleFieldI} } +-- +exampleGetGraphA :: GraphGetRequest +exampleGetGraphA = flip execState emptyGraph $ + modify $ addNode exNodeAVar exNodeA + where + exNodeA = defaultNodeReturn # withLabelQ ''ExampleNode + # withProp ("exampleFieldT", T "A") + # withReturn ["exampleFieldI"] + +-- | Put 'examplePutGraph' to the database. +-- +putGraph :: IO () +putGraph = do + putGraphR <- runQueryDB $ makeRequest @PutRequestB [] examplePutGraph + putStrLn "Uploaded graph: " + print putGraphR + +-- Get 'exampleGetGraphB' and parse it to Haskell object. +-- +getGraphB :: IO () +getGraphB = do + getGraphsR <- runQueryDB $ makeRequest @GetRequestB [] exampleGetGraphB + let nodesA :: [ExampleNode] = fromNode . (! exNodeAVar) . _vertices <$> getGraphsR + putStrLn "Downloaded graph and converted to Haskell object: " + print nodesA + +-- Get 'exampleGetGraphA' and parse it to JSON. +-- +getGraphA :: IO () +getGraphA = do + getGraphsR <- runQueryDB $ makeRequest @GetRequestA [] exampleGetGraphA + let nodesA = (! exNodeAVar) . _vertices <$> getGraphsR + putStrLn "Downloaded graph and converted to JSON: " + B.putStrLn . encode $ nodesA + +main :: IO () +main = putGraph >> getGraphB >> getGraphA diff --git a/hasbolt-extras.cabal b/hasbolt-extras.cabal index 85fb40d..8968597 100644 --- a/hasbolt-extras.cabal +++ b/hasbolt-extras.cabal @@ -1,5 +1,5 @@ name: hasbolt-extras -version: 0.0.0.14 +version: 0.0.0.15 synopsis: Extras for hasbolt library description: Extras for hasbolt library homepage: https://github.com/biocad/hasbolt-extras#readme @@ -64,3 +64,18 @@ library , unordered-containers ghc-options: -Wall -O2 default-language: Haskell2010 + +executable example + hs-source-dirs: example + main-is: Main.hs + build-depends: base >= 4.7 && < 5 + , aeson + , bytestring + , containers + , data-default + , hasbolt + , hasbolt-extras + , mtl + , text + default-language: Haskell2010 + ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall -O2 diff --git a/src/Database/Bolt/Extras/Graph/Internal/AbstractGraph.hs b/src/Database/Bolt/Extras/Graph/Internal/AbstractGraph.hs index 98e2703..467d7ce 100644 --- a/src/Database/Bolt/Extras/Graph/Internal/AbstractGraph.hs +++ b/src/Database/Bolt/Extras/Graph/Internal/AbstractGraph.hs @@ -1,4 +1,6 @@ -{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} module Database.Bolt.Extras.Graph.Internal.AbstractGraph ( @@ -8,10 +10,15 @@ module Database.Bolt.Extras.Graph.Internal.AbstractGraph , emptyGraph , addNode , addRelation + , NodeName + , relationName ) where import Control.Lens (makeLenses, over) import Data.Map.Strict (Map, insert, notMember) +import Data.Monoid ((<>)) +import Data.Text (Text) +import GHC.Generics (Generic) import Text.Printf (printf) -- | 'Graph' contains vertices, that are parameterized by some type @n@, and relations, @@ -19,7 +26,7 @@ import Text.Printf (printf) -- data Graph n a b = Graph { _vertices :: Map n a , _relations :: Map (n, n) b - } deriving (Show) + } deriving (Show, Generic) makeLenses ''Graph @@ -43,3 +50,12 @@ addRelation :: (Show n, Ord n) => n -> n -> b -> Graph n a b -> Graph n a b addRelation startName endName rel graph = if (startName, endName) `notMember` _relations graph then over relations (insert (startName, endName) rel) graph else error $ printf "relation with names (%s, %s) already exists" (show startName) (show endName) + +-- | Alias for text node name. +-- +type NodeName = Text + +-- | Creates relationship name from the names of its start and end nodes +-- in the way `0`. +relationName :: (NodeName, NodeName) -> Text +relationName (st, en) = st <> "0" <> en diff --git a/src/Database/Bolt/Extras/Graph/Internal/Class.hs b/src/Database/Bolt/Extras/Graph/Internal/Class.hs index 2288589..c820dd3 100644 --- a/src/Database/Bolt/Extras/Graph/Internal/Class.hs +++ b/src/Database/Bolt/Extras/Graph/Internal/Class.hs @@ -12,14 +12,15 @@ import Database.Bolt (BoltActionT, Record) -- | Class describes entity, which can be requested. -- class Requestable a where - -- | Condition for BoltId like "ID(a) = b" if BoltId is presented. - maybeBoltIdCond :: a -> Maybe Text -- | How to convert entity to Cypher. request :: a -> Text -- | Class describes entity, which can be returned. -- class Returnable a where + -- | If the entity should be returned. + isReturned' :: a -> Bool + -- | How to return entity in the Cypher. return' :: a -> Text diff --git a/src/Database/Bolt/Extras/Graph/Internal/Get.hs b/src/Database/Bolt/Extras/Graph/Internal/Get.hs index 600b801..89c3a1d 100644 --- a/src/Database/Bolt/Extras/Graph/Internal/Get.hs +++ b/src/Database/Bolt/Extras/Graph/Internal/Get.hs @@ -6,19 +6,25 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} {-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Database.Bolt.Extras.Graph.Internal.Get ( - NodeName -- * Types for requesting nodes and relationships - , NodeGetter (..) + NodeGetter (..) , RelGetter (..) , GetterLike (..) , (#) , defaultNode , defaultRel + , defaultNodeReturn + , defaultNodeNotReturn + , defaultRelReturn + , defaultRelNotReturn + , requestGetters + , allProps -- * Types for extracting nodes and relationships , NodeResult (..) , RelResult (..) @@ -48,7 +54,8 @@ import Data.Map.Strict as M (Map, insert, toList, (!)) -import Data.Maybe (fromJust, +import Data.Maybe (catMaybes, + fromJust, isJust) import Data.Monoid ((<>)) import Data.Text (Text, cons, @@ -64,80 +71,96 @@ import Database.Bolt.Extras (BoltId, GetB NodeLike (..), ToCypher (..), URelationLike (..)) -import Database.Bolt.Extras.Graph.Internal.AbstractGraph (Graph) +import Database.Bolt.Extras.Graph.Internal.AbstractGraph (Graph, + NodeName, + relationName) import Database.Bolt.Extras.Graph.Internal.Class (Extractable (..), Requestable (..), Returnable (..)) import GHC.Generics (Generic) +import Language.Haskell.TH.Syntax (Name, + nameBase) import NeatInterpolation (text) import Text.Printf (printf) --- | Alias for node name --- -type NodeName = Text - ---------------------------------------------------------- -- REQUEST -- ---------------------------------------------------------- -- | Helper to find 'Node's. -- -data NodeGetter = NodeGetter { ngboltId :: Maybe BoltId - , ngLabels :: [Label] - , ngProps :: Map Text B.Value - , ngReturnProps :: [Text] +data NodeGetter = NodeGetter { ngboltId :: Maybe BoltId -- ^ known boltId + , ngLabels :: [Label] -- ^ known labels + , ngProps :: Map Text B.Value -- ^ known properties + , ngReturnProps :: [Text] -- ^ names of properties to return + , ngIsReturned :: Bool -- ^ whether return this node or not } deriving (Show, Eq) -- | Helper to find 'URelationship's. -- -data RelGetter = RelGetter { rgboltId :: Maybe BoltId - , rgLabel :: Maybe Label - , rgProps :: Map Text B.Value - , rgReturnProps :: [Text] +data RelGetter = RelGetter { rgboltId :: Maybe BoltId -- ^ known boltId + , rgLabel :: Maybe Label -- ^ known labels + , rgProps :: Map Text B.Value -- ^ known properties + , rgReturnProps :: [Text] -- ^ names of properties to return + , rgIsReturned :: Bool -- ^ whether return this relation or not } deriving (Show, Eq) (#) :: a -> (a -> b) -> b (#) = (&) -defaultNode :: NodeGetter +defaultNode :: Bool -> NodeGetter defaultNode = NodeGetter Nothing [] (fromList []) [] -defaultRel :: RelGetter +defaultRel :: Bool -> RelGetter defaultRel = RelGetter Nothing Nothing (fromList []) [] +defaultNodeReturn :: NodeGetter +defaultNodeReturn = defaultNode True + +defaultNodeNotReturn :: NodeGetter +defaultNodeNotReturn = defaultNode False + +defaultRelReturn :: RelGetter +defaultRelReturn = defaultRel True + +defaultRelNotReturn :: RelGetter +defaultRelNotReturn = defaultRel False + -- | Helper to work with Getters. -- class GetterLike a where - withBoltId :: BoltId -> a -> a - withLabel :: Label -> a -> a - withProp :: (Text, B.Value) -> a -> a - withReturn :: [Text] -> a -> a + withBoltId :: BoltId -> a -> a -- ^ set known boltId + withLabel :: Label -> a -> a -- ^ set known label + withLabelQ :: Name -> a -> a -- ^ set known label as 'Name' + withProp :: (Text, B.Value) -> a -> a -- ^ add known property + withReturn :: [Text] -> a -> a -- ^ add list of properties to return + isReturned :: a -> a -- ^ set that current node should be returned instance GetterLike NodeGetter where - withBoltId boltId ng = ng { ngboltId = Just boltId } - withLabel lbl ng = ng { ngLabels = lbl : ngLabels ng } - withProp (pk, pv) ng = ng { ngProps = insert pk pv (ngProps ng) } - withReturn props ng = ng { ngReturnProps = ngReturnProps ng ++ props } + withBoltId boltId ng = ng { ngboltId = Just boltId } + withLabel lbl ng = ng { ngLabels = lbl : ngLabels ng } + withLabelQ lblQ = withLabel (pack . nameBase $ lblQ) + withProp (pk, pv) ng = ng { ngProps = insert pk pv (ngProps ng) } + withReturn props ng = ng { ngReturnProps = ngReturnProps ng ++ props } + isReturned ng = ng { ngIsReturned = True } instance GetterLike RelGetter where - withBoltId boltId rg = rg { rgboltId = Just boltId } - withLabel lbl rg = rg { rgLabel = Just lbl } - withProp (pk, pv) rg = rg { rgProps = insert pk pv (rgProps rg) } - withReturn props rg = rg { rgReturnProps = rgReturnProps rg ++ props } + withBoltId boltId rg = rg { rgboltId = Just boltId } + withLabel lbl rg = rg { rgLabel = Just lbl } + withLabelQ lblQ = withLabel (pack . nameBase $ lblQ) + withProp (pk, pv) rg = rg { rgProps = insert pk pv (rgProps rg) } + withReturn props rg = rg { rgReturnProps = rgReturnProps rg ++ props } + isReturned rg = rg { rgIsReturned = True } instance Requestable (NodeName, NodeGetter) where - maybeBoltIdCond (name, ng) = pack . printf "ID(%s)=%d" name <$> ngboltId ng - request (name, ng) = [text|($name $labels $propsQ)|] where labels = toCypher . ngLabels $ ng propsQ = "{" <> (toCypher . toList . ngProps $ ng) <> "}" instance Requestable ((NodeName, NodeName), RelGetter) where - maybeBoltIdCond (names, rg) = pack . printf "ID(%s)=%d" (relationName names) <$> rgboltId rg - request ((stName, enName), rg) = [text|($stName)-[$name $typeQ $propsQ]-($enName)|] where name = relationName (stName, enName) @@ -145,35 +168,57 @@ instance Requestable ((NodeName, NodeName), RelGetter) where propsQ = "{" <> (toCypher . toList . rgProps $ rg) <> "}" instance Returnable (NodeName, NodeGetter) where - return' (name, ng) = let showProps = showRetProps name $ ngReturnProps ng - in [text|{ id: id($name), - labels: labels($name), - props: $showProps - } as $name - |] + isReturned' (_, ng) = ngIsReturned ng + + return' (name, ng) = let showProps = showRetProps name $ ngReturnProps ng + in [text|{ id: id($name), + labels: labels($name), + props: $showProps + } as $name + |] instance Returnable ((NodeName, NodeName), RelGetter) where + isReturned' (_, rg) = rgIsReturned rg + return' ((stName, enName), rg) = let name = relationName (stName, enName) showProps = showRetProps name $ rgReturnProps rg - in [text|{ id: id($name), - label: type($name), - props: $showProps - } as $name - |] + in [text|{ id: id($name), + label: type($name), + props: $showProps + } as $name + |] --- | Creates relationship name from the names of its start and end nodes --- in the way `0`. -relationName :: (NodeName, NodeName) -> Text -relationName (st, en) = st <> "0" <> en +allProps :: [Text] +allProps = ["*"] showRetProps :: Text -> [Text] -> Text -showRetProps name [] = "properties(" <> name <> ")" +showRetProps name [] = name <> "{}" +showRetProps name ["*"] = "properties(" <> name <> ")" showRetProps name props = name <> "{" <> intercalate ", " (cons '.' <$> props) <> "}" +-- | Takes all node getters and relationship getters +-- and write them to single query to request. +-- Also return conditions on known boltId-s. +-- +requestGetters :: [(NodeName, NodeGetter)] + -> [((NodeName, NodeName), RelGetter)] + -> (Text, [Text]) +requestGetters ngs rgs = ("MATCH " <> intercalate ", " (fmap request rgs ++ fmap request ngs), conditionsID) + where + boltIdCondN :: (NodeName, NodeGetter) -> Maybe Text + boltIdCondN (name, ng) = pack . printf "ID(%s)=%d" name <$> ngboltId ng + + boltIdCondR :: ((NodeName, NodeName), RelGetter) -> Maybe Text + boltIdCondR (names, rg) = pack . printf "ID(%s)=%d" (relationName names) <$> rgboltId rg + + conditionsID = catMaybes (fmap boltIdCondN ngs ++ fmap boltIdCondR rgs) + ---------------------------------------------------------- -- RESULT -- ---------------------------------------------------------- +-- | AESON FORMAT + -- | Result for node in the Aeson like format. -- data NodeResult = NodeResult { nresId :: BoltId @@ -216,13 +261,16 @@ instance Extractable NodeResult where instance Extractable RelResult where extract = extractFromJSON +---------------------------------------------------------- +-- | BOLT FORMAT + instance Extractable Node where extract :: forall m. MonadIO m => Text -> [Record] -> BoltActionT m [Node] - extract t rec = (toNode <$>) <$> (extractFromJSON t rec :: BoltActionT m [NodeResult]) + extract t rec = fmap toNode <$> extractFromJSON @ _ @ NodeResult t rec instance Extractable URelationship where extract :: forall m. MonadIO m => Text -> [Record] -> BoltActionT m [URelationship] - extract t rec = (toURelation <$>) <$> (extractFromJSON t rec :: BoltActionT m [RelResult]) + extract t rec = fmap toURelation <$> extractFromJSON @ _ @ RelResult t rec extractFromJSON :: (MonadIO m, FromJSON a) => Text -> [Record] -> BoltActionT m [a] extractFromJSON var = pure . fmap (\r -> case fromJSON (toJSON (r ! var)) of @@ -238,8 +286,8 @@ instance NodeLike NodeResult where fromNode Node{..} = NodeResult nodeIdentity labels (toJSON <$> nodeProps) instance URelationLike RelResult where - toURelation RelResult{..} = URelationship rresId rresLabel (fromJust <$> M.filter isJust (fromJSONM <$> rresProps)) - fromURelation URelationship{..} = RelResult urelIdentity urelType (toJSON <$> urelProps) + toURelation RelResult{..} = URelationship rresId rresLabel (fromJust <$> M.filter isJust (fromJSONM <$> rresProps)) + fromURelation URelationship{..} = RelResult urelIdentity urelType (toJSON <$> urelProps) ---------------------------------------------------------- -- GRAPH TYPES -- diff --git a/src/Database/Bolt/Extras/Graph/Internal/GraphQuery.hs b/src/Database/Bolt/Extras/Graph/Internal/GraphQuery.hs index 06de479..0bc0594 100644 --- a/src/Database/Bolt/Extras/Graph/Internal/GraphQuery.hs +++ b/src/Database/Bolt/Extras/Graph/Internal/GraphQuery.hs @@ -13,6 +13,7 @@ module Database.Bolt.Extras.Graph.Internal.GraphQuery GraphQuery (..) , GetRequestA (..) , GetRequestB (..) + , PutRequestB (..) , mergeGraphs ) where @@ -20,35 +21,36 @@ import Control.Lens (over, (^.)) import Control.Monad.IO.Class (MonadIO) import Data.List (foldl') import Data.Map.Strict (fromList, - keys, mapKeys, mapWithKey, toList, union, (!)) -import Data.Maybe (catMaybes) import Data.Monoid ((<>)) -import Data.Text (Text, - intercalate, - pack) +import Data.Text as T (Text, intercalate, + null, + pack) import Database.Bolt (BoltActionT, Node, Record, URelationship, query) -import Database.Bolt.Extras (GetBoltId (..)) +import Database.Bolt.Extras (BoltId, GetBoltId (..)) import Database.Bolt.Extras.Graph.Internal.AbstractGraph (Graph (..), + NodeName, emptyGraph, + relationName, relations, vertices) import Database.Bolt.Extras.Graph.Internal.Class (Extractable (..), Requestable (..), Returnable (..)) import Database.Bolt.Extras.Graph.Internal.Get (NodeGetter, - NodeName, NodeResult, RelGetter, RelResult, - relationName) + requestGetters) +import Database.Bolt.Extras.Graph.Internal.Put (PutNode, PutRelationship, + requestPut) import NeatInterpolation (text) -- | Type family used to perform requests to the Neo4j based on graphs. @@ -63,10 +65,14 @@ class GraphQuery a where -- | Type of relationship entity, which will be extracted from result. type RelRes a :: * - -- | 'MATCH' or 'MERGE' or 'CREATE' - clause :: Text + -- | How to convert requestable entities to text in the query. + requestEntities :: (Requestable (NodeName, NodeReq a), + Requestable ((NodeName, NodeName), RelReq a)) + => [(NodeName, NodeReq a)] + -> [((NodeName, NodeName), RelReq a)] + -> (Text, [Text]) - -- | Abstract function to form query for get request. + -- | Abstract function to form query for request. -- formQuery :: (Requestable (NodeName, NodeReq a), Requestable ((NodeName, NodeName), RelReq a), @@ -75,27 +81,22 @@ class GraphQuery a where => [Text] -> Graph NodeName (NodeReq a) (RelReq a) -> Text - formQuery customConds graph = [text|$clause' $completeRequest + formQuery customConds graph = [text|$completeRequest $conditionsQ - RETURN $completeReturn|] + RETURN DISTINCT $completeReturn|] where - clause' = clause @a - vertices' = toList (graph ^. vertices) relations' = toList (graph ^. relations) - requestVertices = request <$> vertices' - requestRelations = request <$> relations' + (completeRequest, reqConds) = requestEntities @a vertices' relations' - conditionsID = catMaybes (fmap maybeBoltIdCond vertices' ++ fmap maybeBoltIdCond relations') - conditions = customConds ++ conditionsID - conditionsQ = if null conditions then "" else "WHERE " <> intercalate " AND " conditions + conditions = reqConds ++ customConds + conditionsQ = if Prelude.null conditions then "" else " WHERE " <> intercalate " AND " conditions - returnVertices = return' <$> vertices' - returnRelations = return' <$> relations' + returnVertices = return' <$> filter isReturned' vertices' + returnRelations = return' <$> filter isReturned' relations' - completeRequest = intercalate ", " $ requestVertices ++ requestRelations - completeReturn = intercalate ", " $ returnVertices ++ returnRelations + completeReturn = intercalate ", " $ Prelude.filter (not . T.null) $ returnVertices ++ returnRelations -- | Abstract function, which exctracts graph from records if nodes and relations can be extracted. -- @@ -124,7 +125,14 @@ class GraphQuery a where -> BoltActionT m [Graph NodeName (NodeRes a) (RelRes a)] makeRequest conds graph = do response <- query $ formQuery @a conds graph - extractGraphs @a (keys $ graph ^. vertices) (keys $ graph ^. relations) response + extractGraphs @a presentedVertices presentedRelations response + where + presentedVertices = fmap fst . filter isReturned' . toList $ graph ^. vertices + presentedRelations = fmap fst . filter isReturned' . toList $ graph ^. relations + +--------------------------------------------------------------------------------------- +-- GET -- +--------------------------------------------------------------------------------------- -- | Get request with result in Aeson format. -- Easy way to show result graphs. @@ -141,14 +149,29 @@ instance GraphQuery GetRequestA where type RelReq GetRequestA = RelGetter type NodeRes GetRequestA = NodeResult type RelRes GetRequestA = RelResult - clause = "MATCH" + requestEntities = requestGetters instance GraphQuery GetRequestB where type NodeReq GetRequestB = NodeGetter type RelReq GetRequestB = RelGetter type NodeRes GetRequestB = Node type RelRes GetRequestB = URelationship - clause = "MATCH" + requestEntities = requestGetters + +--------------------------------------------------------------------------------------- +-- PUT -- +--------------------------------------------------------------------------------------- + +-- | Put request in Bolt format with 'BoltId's of uploaded entities as result. +-- +data PutRequestB = PutRequestB + +instance GraphQuery PutRequestB where + type NodeReq PutRequestB = PutNode + type RelReq PutRequestB = PutRelationship + type NodeRes PutRequestB = BoltId + type RelRes PutRequestB = BoltId + requestEntities = requestPut -- | Helper function to merge graphs of results, i.e. -- if you requested graph A->B->C diff --git a/src/Database/Bolt/Extras/Graph/Internal/Put.hs b/src/Database/Bolt/Extras/Graph/Internal/Put.hs index 4a4f61f..d915b3b 100644 --- a/src/Database/Bolt/Extras/Graph/Internal/Put.hs +++ b/src/Database/Bolt/Extras/Graph/Internal/Put.hs @@ -1,6 +1,8 @@ +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} module Database.Bolt.Extras.Graph.Internal.Put ( @@ -8,31 +10,33 @@ module Database.Bolt.Extras.Graph.Internal.Put , PutRelationship (..) , GraphPutRequest , GraphPutResponse - , putNode - , putRelationship - , putGraph + , requestPut ) where -import Control.Monad (forM) -import Control.Monad.IO.Class (MonadIO) -import Data.Map.Strict (mapWithKey, - toList, (!)) -import qualified Data.Map.Strict as M (map) -import qualified Data.Text as T (Text, - pack) -import Database.Bolt (BoltActionT, - Node (..), - RecordValue (..), +import Data.List (foldl') +import Data.Map.Strict (toList, (!)) +import Data.Monoid ((<>)) +import Data.Text (Text, + intercalate, + pack) +import Database.Bolt (Node (..), RecordValue (..), URelationship (..), Value (..), - at, exact, - query) + exact) import Database.Bolt.Extras (BoltId, ToCypher (..), fromInt) -import Database.Bolt.Extras.Graph.Internal.AbstractGraph (Graph (..)) +import Database.Bolt.Extras.Graph.Internal.AbstractGraph (Graph (..), + NodeName, + relationName) +import Database.Bolt.Extras.Graph.Internal.Class (Extractable (..), + Requestable (..), + Returnable (..)) import NeatInterpolation (text) -type NodeName = T.Text +------------------------------------------------------------------------------------------------ +-- REQUEST -- +------------------------------------------------------------------------------------------------ +-- | BOLT FORMAT -- | 'PutNode' is the wrapper for 'Node' where we can specify if we want to merge or create it. -- @@ -44,82 +48,83 @@ data PutNode = BoltId BoltId | MergeN Node | CreateN Node data PutRelationship = MergeR URelationship | CreateR URelationship deriving (Show) --- | The graph of 'Node's with specified uploading type and 'URelationship's. --- -type GraphPutRequest = Graph NodeName PutNode PutRelationship +instance Requestable (NodeName, PutNode) where + request (name, BoltId boltId) = let showBoltId = pack . show $ boltId + in [text|MATCH ($name) WHERE ID($name) = $showBoltId|] + request (name, MergeN node) = requestNode "MERGE" name node + request (name, CreateN node) = requestNode "CREATE" name node --- | The graph of 'BoltId's corresponding to the nodes and relationships --- which we get after putting 'GraphPutRequest'. --- -type GraphPutResponse = Graph NodeName BoltId BoltId +requestNode :: Text -> NodeName -> Node -> Text +requestNode q name Node{..} = [text|$q ($name $labelsQ {$propsQ})|] + where + labelsQ = toCypher labels + propsQ = toCypher . filter ((/= N ()) . snd) . toList $ nodeProps --- | For given @Node _ labels nodeProps@ makes query MERGE or CREATE depending --- on the type of 'PutNode' and returns 'BoltId' of the loaded 'Node'. --- If we already know 'BoltId' of the 'Node' with such parameters, this function does nothing. --- --- Potentially, if you MERGE some 'Node' and its labels and props are occured in --- several 'Node's, then the result can be not one but several 'Node's, --- so the result of this function will be a list of corresponding 'BoltId's. --- -putNode :: (MonadIO m) => PutNode -> BoltActionT m [BoltId] -putNode ut = case ut of - (BoltId bId) -> pure [bId] - (MergeN node) -> helper (T.pack "MERGE") node - (CreateN node) -> helper (T.pack "CREATE") node +instance Requestable ((NodeName, NodeName), PutRelationship) where + request (names, MergeR urel) = requestURelationship "MERGE" names urel + request (names, CreateR urel) = requestURelationship "CREATE" names urel + +requestURelationship :: Text -> (NodeName, NodeName) -> URelationship -> Text +requestURelationship q (stName, enName) URelationship{..} = + [text|$q ($stName)-[$name $labelQ {$propsQ}]->($enName)|] where - helper :: (MonadIO m) => T.Text -> Node -> BoltActionT m [BoltId] - helper q node = do - let varQ = "n" + name = relationName (stName, enName) + labelQ = toCypher urelType + propsQ = toCypher . toList $ urelProps + +-- | Takes all 'PutNode's and 'PutRelationship's +-- and write them to single query to request. +-- Here "WITH" is used, because you cannot perform +-- "match", "merge" or "create" at the same query. +requestPut :: [(NodeName, PutNode)] + -> [((NodeName, NodeName), PutRelationship)] + -> (Text, [Text]) +requestPut pns prs = (fst fullRequest, []) + where + foldStepN :: (Text, [NodeName]) -> (NodeName, PutNode) -> (Text, [NodeName]) + foldStepN accum pn@(name, _) = foldStep accum name pn + + foldStepR :: (Text, [NodeName]) -> ((NodeName, NodeName), PutRelationship) -> (Text, [NodeName]) + foldStepR accum pr@(names, _) = foldStep accum (relationName names) pr - let labelsQ = toCypher $ labels node - let propsQ = toCypher . filter ((/= N ()) . snd) . toList $ nodeProps node + foldStep :: Requestable a => (Text, [NodeName]) -> NodeName -> a -> (Text, [NodeName]) + foldStep (currentQuery, names) name put = + (currentQuery <> request put <> " WITH " <> intercalate ", " updNames <> " ", updNames) + where + updNames = name : names + + requestNodes = foldl' foldStepN ("", []) pns + fullRequest = foldl' foldStepR requestNodes prs + +instance Returnable (NodeName, PutNode) where + -- always return all nodes + isReturned' _ = True + return' (name, _) = [text|ID($name) AS $name|] + +instance Returnable ((NodeName, NodeName), PutRelationship) where + -- always return all relations + isReturned' _ = True + return' (names, _) = let name = relationName names + in [text|ID($name) AS $name|] - let getQuery = [text|$q ($varQ $labelsQ {$propsQ}) - RETURN ID($varQ) as $varQ|] +------------------------------------------------------------------------------------------------ - records <- query getQuery - forM records $ \record -> do - nodeIdentity' <- record `at` varQ >>= exact - pure $ fromInt nodeIdentity' +---------------------------------------------------------- +-- RESULT -- +---------------------------------------------------------- --- | Every relationship in Bolt protocol starts from one 'Node' and ends in anoter. --- For given starting and ending 'Node's 'BoltId's, and for @URelationship _ urelType urelProps@ --- this method makes MERGE query and then returns the corresponding 'BoltId'. +instance Extractable BoltId where + extract name = mapM (fmap fromInt . exact . (! name)) + +---------------------------------------------------------- +-- GRAPH TYPES -- +---------------------------------------------------------- + +-- | The graph of 'Node's with specified uploading type and 'URelationship's. -- -putRelationship :: (MonadIO m) => BoltId -> PutRelationship -> BoltId -> BoltActionT m BoltId -putRelationship start pr end = case pr of - (MergeR relationship) -> helper (T.pack "MERGE") relationship - (CreateR relationship) -> helper (T.pack "CREATE") relationship - where - helper :: (MonadIO m) => T.Text -> URelationship -> BoltActionT m BoltId - helper q URelationship{..} = do - [record] <- query putQuery - urelIdentity' <- record `at` varQ >>= exact - pure $ fromInt urelIdentity' - where - varQ = "r" - labelQ = toCypher urelType - propsQ = toCypher . toList $ urelProps - startT = T.pack . show $ start - endT = T.pack . show $ end - - putQuery :: T.Text - putQuery = [text|MATCH (a), (b) - WHERE ID(a) = $startT AND ID(b) = $endT - $q (a)-[$varQ $labelQ {$propsQ}]->(b) - RETURN ID($varQ) as $varQ|] - --- | Creates graph using given 'GraphPutRequest'. --- If there were multiple choices while merging given _vertices, the first match is used for connection. +type GraphPutRequest = Graph NodeName PutNode PutRelationship + +-- | The graph of 'BoltId's corresponding to the nodes and relationships +-- which we get after putting 'GraphPutRequest'. -- -putGraph :: (MonadIO m) => GraphPutRequest -> BoltActionT m GraphPutResponse -putGraph requestGraph = do - let vertices' = _vertices requestGraph - let rels = _relations requestGraph - nodes <- sequenceA $ M.map (fmap head . putNode) vertices' - edges <- sequenceA $ - mapWithKey (\key v -> do - let stNode = nodes ! fst key - let endNode = nodes ! snd key - putRelationship stNode v endNode) rels - return $ Graph nodes edges +type GraphPutResponse = Graph NodeName BoltId BoltId