Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 17 additions & 17 deletions .github/workflows/haskell-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,58 +8,58 @@
#
# For more information, see https://github.com/haskell-CI/haskell-ci
#
# version: 0.19.20250917
# version: 0.19.20260209
#
# REGENDATA ("0.19.20250917",["github","hasbolt.cabal"])
# REGENDATA ("0.19.20260209",["github","hasbolt.cabal"])
#
name: Haskell-CI
on:
push:
branches:
- master
pull_request:
branches:
- master
merge_group:
branches:
- master
- push
- pull_request
- merge_group
- workflow_dispatch
jobs:
linux:
name: Haskell-CI - Linux - ${{ matrix.compiler }}
runs-on: ubuntu-24.04
timeout-minutes:
60
container:
image: buildpack-deps:focal
image: buildpack-deps:jammy
continue-on-error: ${{ matrix.allow-failure }}
strategy:
matrix:
include:
- compiler: ghc-9.12.2
compilerKind: ghc
compilerVersion: 9.12.2
setup-method: ghcup
allow-failure: false
- compiler: ghc-9.10.3
compilerKind: ghc
compilerVersion: 9.10.3
setup-method: ghcup
allow-failure: true
allow-failure: false
- compiler: ghc-9.8.4
compilerKind: ghc
compilerVersion: 9.8.4
setup-method: ghcup
allow-failure: true
allow-failure: false
- compiler: ghc-9.6.7
compilerKind: ghc
compilerVersion: 9.6.7
setup-method: ghcup
allow-failure: true
allow-failure: false
- compiler: ghc-9.4.8
compilerKind: ghc
compilerVersion: 9.4.8
setup-method: ghcup
allow-failure: true
allow-failure: false
- compiler: ghc-9.2.8
compilerKind: ghc
compilerVersion: 9.2.8
setup-method: ghcup
allow-failure: true
allow-failure: false
fail-fast: false
steps:
- name: apt-get install
Expand Down
36 changes: 22 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
HasBOLT
=======

[![Travis](https://img.shields.io/travis/zmactep/hasbolt.svg)](https://travis-ci.org/zmactep/hasbolt)
[![GitHub Build](https://github.com/zmactep/hasbolt/workflows/build/badge.svg)](https://github.com/zmactep/hasbolt/actions?query=workflow%3A%22build%22)
[![GitHub Build](https://github.com/zmactep/hasbolt/actions/workflows/haskell-ci.yml/badge.svg)](https://github.com/zmactep/hasbolt/actions/workflows/haskell-ci.yml)
[![hackage](https://img.shields.io/hackage/v/hasbolt.svg)](https://hackage.haskell.org/package/hasbolt)
[![hackage-deps](https://img.shields.io/hackage-deps/v/hasbolt.svg)](https://hackage.haskell.org/package/hasbolt)

Haskell driver for Neo4j 3+ (BOLT protocol)
Haskell driver for Neo4j, BOLT protocol versions 3 and 5.6+.

This library skips BOLT 4 entirely and doesn't implement differences between various 5.x minor
versions, so connection will fail if the server does not accept version proposal.

Documentation
-------------
Expand Down Expand Up @@ -42,16 +43,16 @@ nineties = do records <- query "MATCH (nineties:Movie) WHERE nineties.released >
-- you can use 'queryP' function that takes not only the Cypher request but also
-- a parameters dictionary.
genericABN :: RecordValue a => Text -> BoltActionT IO [a]
genericABN name = do toms' <- queryP "MATCH (tom:Person) WHERE tom.name CONTAINS {name} RETURN tom"
genericABN name = do toms' <- queryP "MATCH (tom:Person) WHERE tom.name CONTAINS $name RETURN tom"
(props ["name" =: name])
nodes <- forM toms' $ \record -> record `at` "tom"
forM nodes $ \node -> nodeProps node `at` "name"

-- Hasbolt has a special 'Node' type to unpack graph nodes. You also can find 'Relationship',
-- 'URelationship' and 'Path' as built-in types.
actorsByNameYear :: Text -> Int -> BoltActionT IO [Node]
actorsByNameYear name year = do toms' <- queryP "MATCH (n:Person {name: {props}.name, born: {props}.born}) RETURN n"
(props ["props" =: props ["name" =: name, "born" =: year]])
actorsByNameYear name year = do toms' <- queryP "MATCH (n:Person {name: $name, born: $born}) RETURN n"
(props ["name" =: name, "born" =: year])
forM toms' $ \record -> record `at` "n"

actorsByName :: Text -> BoltActionT IO [Text]
Expand All @@ -63,14 +64,14 @@ wrongType = genericABN

-- Database server answers with a 'ResponseError' exception on any syntax error or internal database problem.
typoInRequest :: Text -> BoltActionT IO [Text]
typoInRequest name = do toms' <- queryP "MATCH (tom:Person) WHERE tom.name CONTAINS {name} RETURN not_tom"
typoInRequest name = do toms' <- queryP "MATCH (tom:Person) WHERE tom.name CONTAINS $name RETURN not_tom"
(props ["name" =: name])
nodes <- forM toms' $ \record -> record `at` "tom"
forM nodes $ \node -> nodeProps node `at` "name"

-- 'RecordHasNoKey' is thrown in case of a wrong key usage in 'at'.
typoInField :: Text -> BoltActionT IO [Text]
typoInField name = do toms' <- queryP "MATCH (tom:Person) WHERE tom.name CONTAINS {name} RETURN tom"
typoInField name = do toms' <- queryP "MATCH (tom:Person) WHERE tom.name CONTAINS $name RETURN tom"
(props ["name" =: name])
nodes <- forM toms' $ \record -> record `at` "not_tom"
forM nodes $ \node -> nodeProps node `at` "name"
Expand Down Expand Up @@ -112,18 +113,25 @@ Notes

* Do not forget to import `Data.Default` to use default connection configuration.
* `OverloadedStrings` are very welcome, as the library doesn't use `String`s at all.
* You can use `Database.Bolt.Lazy` to work with lazy IO. In this case do not forget to read all the records before you send a next query.
* You can use `Database.Bolt.Lazy` to work with lazy IO. In this case do not forget to read all the
records before you send a next query. *Important*: not compatible with RouterPool.
* See [`test/TransactionSpec.hs`](https://github.com/zmactep/hasbolt/blob/master/test/TransactionSpec.hs) for an example of transactions usage.
* Feel free to implement your own serialization procedures with `Database.Bolt.Serialization` module import.
* Pipes work great with [resource-pool](https://hackage.haskell.org/package/resource-pool).
* For neo4j 3.4+ use `version = 2` in connection configuration. This allows you to use [new datatypes](#new-types).
* For Neo4j clusters, use the built-in `RouterPool` (`connectRouterPool`/`runRouterPool`) which handles topology discovery, connection pooling, and read/write routing. For single-server setups, pipes work great with [resource-pool](https://hackage.haskell.org/package/resource-pool).
* The default BOLT protocol version is v5 (5.6–5.8), which works with Neo4j 5.x. The driver also supports older servers — the handshake will negotiate v3 if the server doesn't support v5.
* For neo4j 3.4+ spatial/temporal types, see [new datatypes](#new-types).
* You can use both syntax variants to create properties dictionaries: `fromList [("born", I 1962)]` or `props ["born" =: 1962]`.
* Note that you have to make a type hint for `Text` values in the second construction, as Haskell cannot deduce it on its own.
* Use `$param` syntax for Cypher parameters (the old `{param}` syntax was removed in Neo4j 5).

New types
---------

Neo4j 3.4+ implements BOLT v2 protocol (that still doesn't have any specification). Code inspection of [neo4j sources](https://github.com/neo4j/neo4j) led me to these new data types in v2. All of them are just structures with different signatures and fields.
Neo4j 3.4+ implements BOLT v2 protocol with spatial and temporal data types. They are already
available in `hasbolt`, since lowest supported version is BOLT v3.

All of them are just structures with different signatures and fields.

* Point2D
```haskell
signature = 'X'
Expand Down Expand Up @@ -203,7 +211,7 @@ Codes of Coordinate Reference Systems:

```haskell
λ> :set -XScopedTypeVariables
λ> pipe <- connect $ def { user = "neo4j", password = "neo4j", version = 2 }
λ> pipe <- connect $ def { user = "neo4j", password = "neo4j" }
λ> point :: Value <- run pipe $ do records <- query "RETURN point({x: 1, y: 2, z: 3}) as point"
(head records) `at` "point"
λ> point
Expand Down
20 changes: 14 additions & 6 deletions hasbolt.cabal
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: hasbolt
version: 0.1.7.2
version: 0.1.8.0
synopsis: Haskell driver for Neo4j 3+ (BOLT protocol)
description:
Haskell driver for Neo4j 3+ (BOLT protocol).
Expand All @@ -20,7 +20,7 @@ description:
.
-Bolt protocol version 3 initial support
.
The code was tested with neo4j versions 3.0 — 3.5 and GrapheneDB service
The code was tested with neo4j versions 3.0 — 5.26 and GrapheneDB service


homepage: https://github.com/zmactep/hasbolt#readme
Expand All @@ -40,34 +40,40 @@ tested-with:
|| ==9.6.7
|| ==9.8.4
|| ==9.10.3
|| ==9.12.2

library
hs-source-dirs: src
exposed-modules: Database.Bolt
, Database.Bolt.Lazy
, Database.Bolt.Lens
, Database.Bolt.Serialization
other-modules: Database.Bolt.Value.Type
, Database.Bolt.Value.Helpers
, Database.Bolt.Value.Instances
, Database.Bolt.Connection.Connection
, Database.Bolt.Connection.Type
, Database.Bolt.Connection.Instances
, Database.Bolt.Connection.RouterPool
other-modules: Database.Bolt.Value.Type
, Database.Bolt.Value.Instances
, Database.Bolt.Connection.Connection
, Database.Bolt.Connection.Pipe
, Database.Bolt.Connection
, Database.Bolt.Connection.RoutingTable
, Database.Bolt.Record
, Database.Bolt.Transaction
build-depends: base >= 4.7 && < 5
, bytestring >= 0.10.8.1 && < 0.13
, text >= 1.2.2.1 && < 2.2
, containers >= 0.5.7.1 && < 0.9
, containers >= 0.6.0.1 && < 0.9
, binary >= 0.8.3.0 && < 1.0
, data-binary-ieee754 >= 0.4.4 && < 0.5
, mtl >= 2.2.0 && < 2.4
, network >= 2.6.3.1 && < 3.3
, crypton-connection >= 0.3.1 && < 0.5
, data-default >= 0.7.1.1 && < 0.9
, deepseq >= 1.4 && < 1.6
, exceptions >= 0.10 && < 0.11
, time >= 1.9 && < 1.16
, async >= 2.2 && < 2.3
if impl(ghc < 8.6)
build-depends: contravariant >= 1.4.1 && < 1.6
if impl(ghc < 8.0)
Expand Down Expand Up @@ -100,6 +106,8 @@ test-suite hasbolt-test
, containers
, binary
, bytestring
, data-default
, time
ghc-options: -threaded -rtsopts -with-rtsopts=-N
default-language: Haskell2010

Expand Down
10 changes: 9 additions & 1 deletion src/Database/Bolt.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@ module Database.Bolt
, BoltError (..), UnpackError (..)
, connect, close, reset
, run, runE, queryP, query, queryP_, query_
, transact
, transact, transactRead
, (=:), props
, Pipe
, BoltCfg (..)
, Value (..), IsValue (..), Structure (..), Record, RecordValue (..), exact, exactMaybe, at
, maybeAt, Node (..), Relationship (..), URelationship (..), Path (..)
, AccessMode(..), RoutingTable(..), ServerAddress(..)
, parseRoutingTable, parseAddress, isExpired
, RouterPool, RouterPoolCfg(..)
, connectRouterPool, closeRouterPool
, runRouterPool, runRouterPoolE, runRouterPoolRead, runRouterPoolReadE
, getRoutingTable
) where

import Database.Bolt.Connection hiding (query, queryP)
import Database.Bolt.Connection.Pipe
import Database.Bolt.Connection.RouterPool
import Database.Bolt.Connection.RoutingTable
import Database.Bolt.Connection.Type
import Database.Bolt.Record
import Database.Bolt.Transaction
Expand Down
27 changes: 19 additions & 8 deletions src/Database/Bolt/Connection.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{-# OPTIONS_GHC -Wwarn=incomplete-uni-patterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

module Database.Bolt.Connection
Expand Down Expand Up @@ -27,7 +27,7 @@ import Control.Monad (void)
import Control.Monad.Except (MonadError (..), runExceptT)
import Control.Monad.Reader (MonadReader (..), runReaderT)
import Control.Monad.Trans (MonadIO (..))
import Data.Map.Strict (Map, empty, fromList)
import Data.Map.Strict (Map, empty, fromList, union)
import Data.Text (Text)
import GHC.Stack (HasCallStack)

Expand Down Expand Up @@ -62,8 +62,13 @@ query' cypher = queryP' cypher empty

-- |Runs Cypher query with parameters and ignores response
queryP_ :: MonadIO m => HasCallStack => Text -> Map Text Value -> BoltActionT m ()
queryP_ cypher params = do void $ sendRequest cypher params empty
ask >>= liftE . discardAll
queryP_ cypher params = do pipe <- ask
void $ sendRequest cypher params empty
let discardReq = if isV5_6 (pipe_version pipe)
then RequestDiscard (fromList ["n" =: (-1 :: Int)])
else RequestDiscardAll
liftE $ do flush pipe discardReq
void $ fetch pipe

-- |Runs Cypher query and ignores response
query_ :: MonadIO m => HasCallStack => Text -> BoltActionT m ()
Expand All @@ -78,11 +83,16 @@ querySL strict cypher params = do keys <- pullKeys cypher params empty
pullKeys :: MonadIO m => HasCallStack => Text -> Map Text Value -> Map Text Value -> BoltActionT m [Text]
pullKeys cypher params ext = do pipe <- ask
status <- sendRequest cypher params ext
liftE $ flush pipe RequestPullAll
let pullReq = if isV5_6 (pipe_version pipe)
then RequestPull (fromList ["n" =: (-1 :: Int)])
else RequestPullAll
liftE $ flush pipe pullReq
mkKeys status
where
mkKeys :: MonadIO m => Response -> BoltActionT m [Text]
mkKeys (ResponseSuccess response) = response `at` "fields" `catchError` \(RecordHasNoKey _) -> pure []
mkKeys (ResponseSuccess response) = response `at` "fields" `catchError` \case
RecordHasNoKey _ -> pure []
e -> throwError e
mkKeys x = throwError $ ResponseError (mkFailure x)

pullRecords :: MonadIO m => HasCallStack => Bool -> [Text] -> BoltActionT m [Record]
Expand Down Expand Up @@ -121,6 +131,7 @@ sendRawRequest req = do
sendRequest :: MonadIO m => HasCallStack => Text -> Map Text Value -> Map Text Value -> BoltActionT m Response
sendRequest cypher params ext =
do pipe <- ask
if isNewVersion (pipe_version pipe)
then sendRawRequest $ RequestRunV3 cypher params ext
if isV3 (pipe_version pipe)
then let nExtra = notifExtra (pipe_version pipe) (pipeNotificationsMinimumSeverity pipe) (pipeNotificationsDisabledCategories pipe)
in sendRawRequest $ RequestRunV3 cypher params (ext `union` nExtra `union` dbExtra (pipeDatabase pipe))
else sendRawRequest $ RequestRun cypher params
Loading
Loading