hypher 0.1.2 → 0.1.3
raw patch · 5 files changed
+70/−185 lines, 5 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Database.Neo4j.Cypher: ParamLiteral :: PropertyValue -> ParamValue
- Database.Neo4j.Cypher: ParamProperties :: Properties -> ParamValue
- Database.Neo4j.Cypher: ParamPropertiesArray :: [Properties] -> ParamValue
- Database.Neo4j.Cypher: Response :: [Text] -> [[Value]] -> Response
- Database.Neo4j.Cypher: cols :: Response -> [Text]
- Database.Neo4j.Cypher: cypher :: Text -> Params -> Neo4j (Either Text Response)
- Database.Neo4j.Cypher: data ParamValue
- Database.Neo4j.Cypher: data Response
- Database.Neo4j.Cypher: fromResult :: Response -> Either Text Response -> Response
- Database.Neo4j.Cypher: fromSuccess :: Either Text Response -> Response
- Database.Neo4j.Cypher: instance Eq ParamValue
- Database.Neo4j.Cypher: instance Eq Response
- Database.Neo4j.Cypher: instance FromJSON Response
- Database.Neo4j.Cypher: instance Show ParamValue
- Database.Neo4j.Cypher: instance Show Response
- Database.Neo4j.Cypher: instance ToJSON ParamValue
- Database.Neo4j.Cypher: isSuccess :: Either Text Response -> Bool
- Database.Neo4j.Cypher: newparam :: PropertyValueConstructor a => a -> ParamValue
- Database.Neo4j.Cypher: type Params = HashMap Text ParamValue
- Database.Neo4j.Cypher: vals :: Response -> [[Value]]
- Database.Neo4j.Graph: addCypher :: Response -> Graph -> Graph
+ Database.Neo4j.Transactional.Cypher: instance Eq ParamValue
+ Database.Neo4j.Transactional.Cypher: instance Show ParamValue
+ Database.Neo4j.Transactional.Cypher: instance ToJSON ParamValue
Files
- hypher.cabal +5/−4
- src/Database/Neo4j/Cypher.hs +0/−91
- src/Database/Neo4j/Graph.hs +0/−16
- src/Database/Neo4j/Transactional/Cypher.hs +21/−1
- tests/IntegrationTests.hs +44/−73
hypher.cabal view
@@ -1,5 +1,5 @@ name: hypher-version: 0.1.2+version: 0.1.3 synopsis: A Haskell neo4j client description: Library to interact with Neo4j databases. @@ -13,8 +13,9 @@ -Batch calls for CRUD operations. . .- All code has been tested with Neo4j versions 2.0.3 and 2.1.5. - This is a fork of: http://hackage.haskell.org/package/haskell-neo4j-client. Attempting to bring it up to date.+ All code has been tested with Neo4j versions 2.0.3 and 2.1.5. + This is a fork of: http://hackage.haskell.org/package/haskell-neo4j-client. Attempting to bring it up to date. + Only supports new Transactional Cypher REST api. homepage: https://github.com/zoetic-community/hyper.git license: MIT@@ -63,7 +64,7 @@ library hs-source-dirs: src- exposed-modules: Database.Neo4j, Database.Neo4j.Graph, Database.Neo4j.Batch, Database.Neo4j.Cypher,+ exposed-modules: Database.Neo4j, Database.Neo4j.Graph, Database.Neo4j.Batch Database.Neo4j.Transactional.Cypher other-modules: Database.Neo4j.Types, Database.Neo4j.Http, Database.Neo4j.Node, Database.Neo4j.Relationship, Database.Neo4j.Property, Database.Neo4j.Label, Database.Neo4j.Index, Database.Neo4j.Batch.Node,
− src/Database/Neo4j/Cypher.hs
@@ -1,91 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}---- | IMPORTANT! MODULE DEPRECATED, better use the code in this same library that uses the Neo4j transactional endpoint--- in "Database.Neo4j.Transactional.Cypher"--- Module to provide Cypher support.--- Currently we allow sending queries with parameters, the result is a collection of column headers--- and JSON data values, the Graph object has the function addCypher that tries to find--- nodes and relationships in a cypher query result and insert them in a "Database.Neo4j.Graph" object------ > import qualified Database.Neo4j.Cypher as C--- >--- > withConnection host port $ do--- > ...--- > -- Run a cypher query with parameters--- > res <- C.cypher "CREATE (n:Person { name : {name} }) RETURN n" M.fromList [("name", C.newparam ("Pep" :: T.Text))]--- >--- > -- Get all nodes and relationships that this query returned and insert them in a Graph object--- > let graph = G.addCypher (C.fromSuccess res) G.empty--- >--- > -- Get the column headers--- > let columnHeaders = C.cols $ C.fromSuccess res--- >--- > -- Get the rows of JSON values received--- > let values = C.vals $ C.fromSuccess res-module Database.Neo4j.Cypher (- -- * Types- Response(..), ParamValue(..), Params, newparam,- -- * Sending queries- cypher, fromResult, fromSuccess, isSuccess- ) where--import Data.Aeson ((.=), (.:))-import Control.Applicative ((<$>), (<*>))-import Control.Monad (mzero)--import qualified Data.Aeson as J-import qualified Data.ByteString as S-import qualified Data.HashMap.Lazy as M-import qualified Data.Text as T--import Database.Neo4j.Types-import Database.Neo4j.Http---- | Type for a Cypher response with tuples containing column name and their values-data Response = Response {cols :: [T.Text], vals :: [[J.Value]]} deriving (Show, Eq)---- | How to create a response object from a cypher JSON response-instance J.FromJSON Response where- parseJSON (J.Object o) = Response <$> (o .: "columns" >>= J.parseJSON) <*> (o .: "data" >>= J.parseJSON)- parseJSON _ = mzero- -cypherAPI :: S.ByteString-cypherAPI = "/db/data/cypher"---- | Value for a cypher parmeter value, might be a literal, a property map or a list of property maps-data ParamValue = ParamLiteral PropertyValue | ParamProperties Properties | ParamPropertiesArray [Properties]- deriving (Show, Eq)--newparam :: PropertyValueConstructor a => a -> ParamValue-newparam = ParamLiteral . newval---- | Instance toJSON for param values so we can serialize them in queries-instance J.ToJSON ParamValue where- toJSON (ParamLiteral l) = J.toJSON l- toJSON (ParamProperties p) = J.toJSON p- toJSON (ParamPropertiesArray ps) = J.toJSON ps---- | We use hashmaps to represent Cypher parameters-type Params = M.HashMap T.Text ParamValue---- | Run a cypher query-cypher :: T.Text -> Params -> Neo4j (Either T.Text Response)-cypher cmd params = Neo4j $ \conn -> httpCreate4XXExplained conn cypherAPI body- where body = J.encode $ J.object ["query" .= cmd, "params" .= J.toJSON params]---- | Get the result of the response or a default value-fromResult :: Response -> Either T.Text Response -> Response-fromResult def (Left _) = def-fromResult _ (Right resp) = resp---- | Get the result of the response or a default value-fromSuccess :: Either T.Text Response -> Response-fromSuccess (Left _) = error "Cypher.fromSuccess but is Error"-fromSuccess (Right resp) = resp---- | True if the operation succeeded-isSuccess :: Either T.Text Response -> Bool-isSuccess (Left _) = False-isSuccess (Right _) = True--{-# DEPRECATED cypher, fromResult, fromSuccess, isSuccess "Use Database.Neo4j.Transactional.Cypher instead" #-}
src/Database/Neo4j/Graph.hs view
@@ -19,8 +19,6 @@ setProperties, setProperty, deleteProperties, deleteProperty, -- * Handling labels setNodeLabels, addNodeLabel, getNodeLabels, deleteNodeLabel,- -- * Handling Cypher results- addCypher, -- * Graph filtering functions nodeFilter, relationshipFilter, -- * Graph operations@@ -38,7 +36,6 @@ import qualified Data.Text as T import Database.Neo4j.Types-import qualified Database.Neo4j.Cypher as C type NodeIndex = M.HashMap NodePath Node type RelIndex = M.HashMap RelPath Relationship@@ -257,16 +254,3 @@ intersection ga gb = relationshipFilter relFilterFunc (nodeFilter nodeFilterFunc ga) where relFilterFunc r = hasRelationship r gb nodeFilterFunc n = hasNode n gb---- | Feed a cypher result (from the old API) into a graph (looks for nodes and relationships and inserts them)-addCypher :: C.Response -> Graph -> Graph-addCypher (C.Response _ vals) ginit = foldl tryAdd ginit (concat vals)- where tryAdd :: Graph -> J.Value -> Graph- -- Try first to parse it as a relationship and then as a node, otherwise leave the graph as it is- tryAdd g v = fromMaybe g $ L.find parseSuccess (map ($ v) [relParser g, nodeParser g]) >>= fromResult- relParser g v = flip addRelationship g <$> J.fromJSON v- nodeParser g v = flip addNode g <$> J.fromJSON v- parseSuccess (J.Success _) = True- parseSuccess (J.Error _) = False- fromResult (J.Error _) = Nothing- fromResult (J.Success g) = Just g
src/Database/Neo4j/Transactional/Cypher.hs view
@@ -29,6 +29,7 @@ loneQuery, runTransaction, cypher, rollback, commit, keepalive, commitWith, rollbackAndLeave, -- * Aux functions isSuccess, fromResult, fromSuccess+ ) where import Control.Monad.Reader@@ -47,15 +48,34 @@ import qualified Data.Acquire as A import qualified Data.ByteString as S import qualified Data.ByteString.Lazy as L+import qualified Data.List as LI import qualified Data.Text as T import qualified Data.Vector as V import qualified Network.HTTP.Types as HT+import qualified Data.HashMap.Lazy as M import Database.Neo4j.Types import Database.Neo4j.Http-import Database.Neo4j.Cypher (ParamValue(..), Params, newparam) import qualified Database.Neo4j.Graph as G +cypherAPI :: S.ByteString+cypherAPI = "/db/data/cypher"++-- | Value for a cypher parmeter value, might be a literal, a property map or a list of property maps+data ParamValue = ParamLiteral PropertyValue | ParamProperties Properties | ParamPropertiesArray [Properties]+ deriving (Show, Eq)++newparam :: PropertyValueConstructor a => a -> ParamValue+newparam = ParamLiteral . newval++-- | Instance toJSON for param values so we can serialize them in queries+instance J.ToJSON ParamValue where+ toJSON (ParamLiteral l) = J.toJSON l+ toJSON (ParamProperties p) = J.toJSON p+ toJSON (ParamPropertiesArray ps) = J.toJSON ps++-- | We use hashmaps to represent Cypher parameters+type Params = M.HashMap T.Text ParamValue -- | Holds the connection stats data Stats = Stats {containsUpdates :: Bool,
tests/IntegrationTests.hs view
@@ -23,7 +23,6 @@ import Database.Neo4j import qualified Database.Neo4j.Batch as B-import qualified Database.Neo4j.Cypher as C import qualified Database.Neo4j.Graph as G import qualified Database.Neo4j.Transactional.Cypher as TC @@ -50,8 +49,7 @@ -- | handy assertBool inside a Neo4j monad neo4jBool :: Bool -> Neo4j ()-neo4jBool f = liftIO $ assertBool "" f-+neo4jBool f = liftIO $ assertBool "Expected true" f main :: IO () main = $(defaultMainGenerator)@@ -1191,24 +1189,23 @@ n2 <- B.createNode $ M.fromList ["name" |: ("you" :: T.Text)] B.createRelationship "know" M.empty n1 n2 -- actual test- res <- C.cypher query params- neo4jBool $ C.isSuccess res- neo4jEqual ["TYPE(r)"] (C.cols $ C.fromSuccess res)- neo4jBool $ ["know"] `elem` (C.vals $ C.fromSuccess res)+ res <- TC.loneQuery query params+ neo4jBool $ TC.isSuccess res+ neo4jEqual ["TYPE(r)"] (TC.cols $ TC.fromSuccess res)+ neo4jBool $ ["know"] `elem` (TC.vals $ TC.fromSuccess res) -- cleanup _ <- B.runBatch $ mapM_ B.deleteRelationship (G.getRelationships gp) _ <- B.runBatch $ mapM_ B.deleteNode (G.getNodes gp) return () where query = "MATCH (x {name: {startName}})-[r]-(friend) WHERE friend.name = {name} RETURN TYPE(r)"- params = M.fromList [("startName", C.newparam ("I" :: T.Text)), ("name", C.newparam ("you" :: T.Text))]+ params = M.fromList [("startName", TC.newparam ("I" :: T.Text)), ("name", TC.newparam ("you" :: T.Text))] -- | Test cypher create a node case_cypherCreateNode :: Assertion case_cypherCreateNode = withConnection host port $ do- res <- C.cypher query params- neo4jBool $ C.isSuccess res- let gp = G.addCypher (C.fromSuccess res) G.empty+ res <- TC.loneQuery query params+ let gp = (head . TC.graph) (TC.fromSuccess res) let props = M.fromList ["name" |: ("Andres" :: T.Text)] neo4jEqual 1 (length $ G.getNodes gp) neo4jBool $ props `elem` map getNodeProperties (G.getNodes gp)@@ -1216,14 +1213,14 @@ _ <- B.runBatch $ mapM_ B.deleteNode (G.getNodes gp) return () where query = "CREATE (n:Person { name : {name} }) RETURN n"- params = M.fromList [("name", C.newparam ("Andres" :: T.Text))]+ params = M.fromList [("name", TC.newparam ("Andres" :: T.Text))] -- | Test cypher create a node with multiple properties case_cypherCreateNodeMultipleProperties :: Assertion case_cypherCreateNodeMultipleProperties = withConnection host port $ do- res <- C.cypher query params- neo4jBool $ C.isSuccess res- let gp = G.addCypher (C.fromSuccess res) G.empty+ res <- TC.loneQuery query params+ neo4jBool $ TC.isSuccess res+ let gp = (head . TC.graph) (TC.fromSuccess res) neo4jEqual 1 (length $ G.getNodes gp) neo4jBool $ props `elem` map getNodeProperties (G.getNodes gp) -- cleanup@@ -1232,31 +1229,14 @@ where query = "CREATE (n:Person { props }) RETURN n" props = M.fromList ["name" |: ("Michael" :: T.Text), "position" |: ("Developer" :: T.Text), "awesome" |: True, "children" |: (3 :: Int64)]- params = M.fromList [("props", C.ParamProperties props)]---- | Test cypher create multiple nodes-case_cypherCreateMultipleNodes :: Assertion-case_cypherCreateMultipleNodes = withConnection host port $ do- res <- C.cypher query params- neo4jBool $ C.isSuccess res- let gp = G.addCypher (C.fromSuccess res) G.empty- neo4jEqual 2 (length $ G.getNodes gp)- neo4jBool $ propsA `elem` map getNodeProperties (G.getNodes gp)- neo4jBool $ propsB `elem` map getNodeProperties (G.getNodes gp)- -- cleanup- _ <- B.runBatch $ mapM_ B.deleteNode (G.getNodes gp)- return ()- where query = "CREATE (n:Person { props }) RETURN n"- propsA = M.fromList ["name" |: ("Andres" :: T.Text), "position" |: ("Developer" :: T.Text)]- propsB = M.fromList ["name" |: ("Michael" :: T.Text), "position" |: ("Developer" :: T.Text)]- params = M.fromList [("props", C.ParamPropertiesArray [propsA, propsB])]+ params = M.fromList [("props", TC.ParamProperties props)] -- | Test cypher set all properties on a node case_cypherSetAllNodeProperties :: Assertion case_cypherSetAllNodeProperties = withConnection host port $ do- res <- C.cypher query params- neo4jBool $ C.isSuccess res- let gp = G.addCypher (C.fromSuccess res) G.empty+ res <- TC.loneQuery query params+ neo4jBool $ TC.isSuccess res+ let gp = (head . TC.graph) (TC.fromSuccess res) neo4jEqual 1 (length $ G.getNodes gp) neo4jBool $ props `elem` map getNodeProperties (G.getNodes gp) -- cleanup@@ -1265,7 +1245,7 @@ where query = "CREATE (n:Person { name: 'this property is to be deleted' } ) SET n = { props } RETURN n" props = M.fromList ["name" |: ("Michael" :: T.Text), "position" |: ("Developer" :: T.Text), "awesome" |: True, "children" |: (3 :: Int64)]- params = M.fromList [("props", C.ParamProperties props)]+ params = M.fromList [("props", TC.ParamProperties props)] -- | Test cypher basic query case_cypherBasicQuery :: Assertion@@ -1278,11 +1258,11 @@ _ <- B.createRelationship "know" M.empty n1 n2 B.createRelationship "know" M.empty n1 n3 -- actual test- res <- C.cypher query params- neo4jBool $ C.isSuccess res- neo4jEqual ["type(r)", "n.name", "n.age"] (C.cols $ C.fromSuccess res)- neo4jBool $ ["know", "him", J.Number 25] `elem` (C.vals $ C.fromSuccess res)- neo4jBool $ ["know", "you", J.Null] `elem` (C.vals $ C.fromSuccess res)+ res <- TC.loneQuery query params+ neo4jBool $ TC.isSuccess res+ neo4jEqual ["type(r)", "n.name", "n.age"] (TC.cols $ TC.fromSuccess res)+ neo4jBool $ ["know", "him", J.Number 25] `elem` (TC.vals $ TC.fromSuccess res)+ neo4jBool $ ["know", "you", J.Null] `elem` (TC.vals $ TC.fromSuccess res) -- cleanup _ <- B.runBatch $ mapM_ B.deleteRelationship (G.getRelationships gp) _ <- B.runBatch $ mapM_ B.deleteNode (G.getNodes gp)@@ -1298,18 +1278,23 @@ n1 <- B.createNode $ M.fromList ["name" |: ("I" :: T.Text)] n2 <- B.createNode $ M.fromList ["name" |: ("you" :: T.Text)] n3 <- B.createNode $ M.fromList ["name" |: ("him" :: T.Text), "age" |: (25 :: Int64)]- _ <- B.createRelationship "know" M.empty n1 n2- B.createRelationship "know" M.empty n1 n3+ B.createRelationship "KNOW" M.empty n1 n2+ B.createRelationship "KNOW" M.empty n1 n3+ -- actual test- res <- C.cypher query params- neo4jBool $ C.isSuccess res- let gres = G.addCypher (C.fromSuccess res) G.empty- neo4jBool $ (length $ G.getRelationships gres) >= 2- neo4jBool $ all (\r -> getRelType r == "know") (G.getRelationships gres)- neo4jEqual ["r"] (C.cols $ C.fromSuccess res)+ res <- TC.loneQuery query params+ TC.loneQuery "MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n,r" M.empty++ neo4jBool $ TC.isSuccess res+ let row1 = (head . TC.graph . TC.fromSuccess) res+ let row2 = (head . tail . TC.graph . TC.fromSuccess) res++ neo4jEqual 1 (length $ G.getRelationships row1)+ neo4jEqual 1 (length $ G.getRelationships row2)+ neo4jBool $ all (\r -> getRelType r == "KNOW") (G.getRelationships row1)+ neo4jBool $ all (\r -> getRelType r == "KNOW") (G.getRelationships row2)+ neo4jEqual ["r"] (TC.cols $ TC.fromSuccess res) -- cleanup- _ <- B.runBatch $ mapM_ B.deleteRelationship (G.getRelationships gp)- _ <- B.runBatch $ mapM_ B.deleteNode (G.getNodes gp) return () where query = "MATCH (x {name: 'I'})-[r]->(n) RETURN r" params = M.empty@@ -1323,10 +1308,10 @@ n2 <- B.createNode $ M.fromList ["name" |: ("you" :: T.Text)] B.createRelationship "know" M.empty n1 n2 -- actual test- res <- C.cypher query params- neo4jBool $ C.isSuccess res- neo4jEqual ["collect(n.name)"] (C.cols $ C.fromSuccess res)- let v = case (head $ head $ C.vals $ C.fromSuccess res) of+ res <- TC.loneQuery query params+ neo4jBool $ TC.isSuccess res+ neo4jEqual ["collect(n.name)"] (TC.cols $ TC.fromSuccess res)+ let v = case (head $ head $ TC.vals $ TC.fromSuccess res) of J.Array vec -> vec _ -> V.empty neo4jBool $ "you" `V.elem` v@@ -1338,21 +1323,6 @@ where query = "MATCH (n) WHERE n.name in ['I', 'you'] RETURN collect(n.name)" params = M.empty --- | Test cypher errors-case_cypherError :: Assertion-case_cypherError = withConnection host port $ do- -- create initial data- gp <- B.runBatch $ do- B.createNode $ M.fromList ["age" |: (26 :: Int64)]- -- actual test- res <- C.cypher query params- neo4jBool $ res `elem` [Left "BadInputException", Left "ArithmeticException"]- -- cleanup- _ <- B.runBatch $ mapM_ B.deleteNode (G.getNodes gp)- return ()- where query = "MATCH (x) WHERE has(x.age) RETURN x.age / 0"- params = M.empty- -- | Test lone transaction cypher basic query case_loneTransactionBasicQuery :: Assertion case_loneTransactionBasicQuery = withConnection host port $ do@@ -1395,8 +1365,9 @@ case_cypherTransaction :: Assertion case_cypherTransaction = withConnection host port $ do res <- TC.runTransaction $ do- result <- TC.cypher "CREATE (pere: PERSON {age: {age}}) CREATE (pau: PERSON {props}) \- \CREATE p1 = (pere)-[:KNOWS]->(pau) RETURN pere, pau, p1, pere.age" $+ result <- TC.cypher "CREATE (pere: PERSON {age: {age}}) \ + \CREATE (pau: PERSON {props}) \+ \CREATE p1 = (pere)-[:KNOWS]->(pau) RETURN pere, pau, p1, pere.age" $ M.fromList [("age", TC.newparam (78 :: Int64)), ("props", TC.ParamProperties $ M.fromList["age" |: (99 :: Int64)])] voidIO $ assertEqual "" ["pere", "pau", "p1", "pere.age"] (TC.cols result)