hasbolt-extras 0.0.1.0 → 0.0.1.1
raw patch · 12 files changed
+249/−17 lines, 12 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
+ Database.Bolt.Extras.DSL: [nodeParams] :: NodeSelector -> [(Text, Text)]
+ Database.Bolt.Extras.DSL: [relParams] :: RelSelector -> [(Text, Text)]
+ Database.Bolt.Extras.DSL: withParam :: SelectorLike a => (Text, Text) -> a -> a
+ Database.Bolt.Extras.DSL.Typed: -- collection, with any type.
+ Database.Bolt.Extras.DSL.Typed: CypherDSLParams :: CypherDSL a -> CypherDSLParams
+ Database.Bolt.Extras.DSL.Typed: class QueryWithParams (params :: [(Symbol, Type)]) (m :: Type -> Type) fun | params m -> fun
+ Database.Bolt.Extras.DSL.Typed: collectParams :: QueryWithParams params m fun => CypherDSL () -> [(Text, Value)] -> fun
+ Database.Bolt.Extras.DSL.Typed: newtype CypherDSLParams (params :: [(Symbol, Type)]) (a :: Type)
+ Database.Bolt.Extras.DSL.Typed: param :: forall (field :: Symbol) (a :: k -> Type) (types :: k). SelectorLike a => HasField' types field => (SymbolS field, Text) -> a types -> a types
+ Database.Bolt.Extras.DSL.Typed: queryWithParams :: forall params m fun. MonadIO m => QueryWithParams params m fun => CypherDSLParams params () -> fun
+ Database.Bolt.Extras.DSL.Typed: withParam :: (SelectorLike a, HasField' types field) => (SymbolS field, Text) -> a types -> a types
- Database.Bolt.Extras.DSL: NodeSelector :: Maybe Text -> [Text] -> [(Text, Value)] -> NodeSelector
+ Database.Bolt.Extras.DSL: NodeSelector :: Maybe Text -> [Text] -> [(Text, Value)] -> [(Text, Text)] -> NodeSelector
- Database.Bolt.Extras.DSL: RelSelector :: Maybe Text -> Text -> [(Text, Value)] -> RelSelector
+ Database.Bolt.Extras.DSL: RelSelector :: Maybe Text -> Text -> [(Text, Value)] -> [(Text, Text)] -> RelSelector
- Database.Bolt.Extras.DSL: type CypherDSL a = Free Expr ()
+ Database.Bolt.Extras.DSL: type CypherDSL a = Free Expr a
- Database.Bolt.Extras.DSL.Typed: -- | This constraint checks that field with this name has correct type in
+ Database.Bolt.Extras.DSL.Typed: -- | This constraint checks that field with this name exists in the
- Database.Bolt.Extras.DSL.Typed: type family HasField (types :: k) (field :: Symbol) (typ :: Type) :: Constraint;
+ Database.Bolt.Extras.DSL.Typed: type family HasField' (types :: k) (field :: Symbol) :: Constraint;
- Database.Bolt.Extras.Graph: relations :: forall n_abh1 a_abh2 b_abh3 b_albi. Lens (Graph n_abh1 a_abh2 b_abh3) (Graph n_abh1 a_abh2 b_albi) (Map (n_abh1, n_abh1) b_abh3) (Map (n_abh1, n_abh1) b_albi)
+ Database.Bolt.Extras.Graph: relations :: forall n_aciB a_aciC b_aciD b_alcp. Lens (Graph n_aciB a_aciC b_aciD) (Graph n_aciB a_aciC b_alcp) (Map (n_aciB, n_aciB) b_aciD) (Map (n_aciB, n_aciB) b_alcp)
- Database.Bolt.Extras.Graph: vertices :: forall n_abh1 a_abh2 b_abh3 a_albj. Lens (Graph n_abh1 a_abh2 b_abh3) (Graph n_abh1 a_albj b_abh3) (Map n_abh1 a_abh2) (Map n_abh1 a_albj)
+ Database.Bolt.Extras.Graph: vertices :: forall n_aciB a_aciC b_aciD a_alcq. Lens (Graph n_aciB a_aciC b_aciD) (Graph n_aciB a_alcq b_aciD) (Map n_aciB a_aciC) (Map n_aciB a_alcq)
Files
- CHANGELOG.md +5/−0
- hasbolt-extras.cabal +2/−1
- src/Database/Bolt/Extras/DSL/Internal/Instances.hs +34/−8
- src/Database/Bolt/Extras/DSL/Internal/Language.hs +1/−1
- src/Database/Bolt/Extras/DSL/Internal/Types.hs +5/−2
- src/Database/Bolt/Extras/DSL/Typed.hs +40/−4
- src/Database/Bolt/Extras/DSL/Typed/Families.hs +6/−0
- src/Database/Bolt/Extras/DSL/Typed/Instances.hs +20/−1
- src/Database/Bolt/Extras/DSL/Typed/Parameters.hs +97/−0
- src/Database/Bolt/Extras/DSL/Typed/Types.hs +33/−0
- src/Database/Bolt/Extras/Internal/Cypher.hs +5/−0
- test/Doctest.hs +1/−0
CHANGELOG.md view
@@ -6,6 +6,11 @@ ## [Unreleased] +## [0.0.1.1] - 2019-12-31+### Added+- `param` combinator to add named parameters to selectors;+- `CypherDSLParams` to control parameters that queries accept.+ ## [0.0.1.0] - 2019-12-17 ### Changed - Use `hasbolt` 0.1.4.0.
hasbolt-extras.cabal view
@@ -1,5 +1,5 @@ name: hasbolt-extras-version: 0.0.1.0+version: 0.0.1.1 synopsis: Extras for hasbolt library description: Extras for hasbolt library homepage: https://github.com/biocad/hasbolt-extras#readme@@ -44,6 +44,7 @@ , Database.Bolt.Extras.DSL.Typed.Types , Database.Bolt.Extras.DSL.Typed.Families , Database.Bolt.Extras.DSL.Typed.Instances+ , Database.Bolt.Extras.DSL.Typed.Parameters , Database.Bolt.Extras.Graph.Internal.AbstractGraph , Database.Bolt.Extras.Graph.Internal.Class
src/Database/Bolt/Extras/DSL/Internal/Instances.hs view
@@ -34,11 +34,13 @@ withIdentifier idx node = node { nodeIdentifier = Just idx } withLabel lbl node = node { nodeLabels = lbl : nodeLabels node } withProp prop node = node { nodeProperties = prop : nodeProperties node }+ withParam prop node = node { nodeParams = prop : nodeParams node } instance SelectorLike RelSelector where withIdentifier idx rel = rel { relIdentifier = Just idx } withLabel lbl rel = rel { relLabel = lbl } withProp prop rel = rel { relProperties = prop : relProperties rel }+ withParam prop rel = rel { relParams = prop : relParams rel } instance ToCypher NodeSelector where toCypher NodeSelector{..} = execWriter $ do@@ -50,10 +52,22 @@ [] -> pure () _ -> tell $ toCypher nodeLabels case nodeProperties of- [] -> pure ()- _ -> do tell "{"- tell $ toCypher nodeProperties- tell "}"+ [] -> case nodeParams of+ [] -> pure ()+ _ -> do+ tell "{"+ tell $ toCypher nodeParams+ tell "}"+ _ -> do+ tell "{"+ tell $ toCypher nodeProperties+ case nodeParams of+ [] -> pure ()+ _ -> do+ tell ","+ tell $ toCypher nodeParams+ tell "}"+ tell ")" instance ToCypher RelSelector where@@ -66,10 +80,22 @@ "" -> pure () _ -> tell $ toCypher relLabel case relProperties of- [] -> pure ()- _ -> do tell "{"- tell $ toCypher relProperties- tell "}"+ [] -> case relParams of+ [] -> pure ()+ _ -> do+ tell "{"+ tell $ toCypher relParams+ tell "}"+ _ -> do+ tell "{"+ tell $ toCypher relProperties+ case relParams of+ [] -> pure ()+ _ -> do+ tell ","+ tell $ toCypher relParams+ tell "}"+ tell "]" instance ToCypher PathSelector where
src/Database/Bolt/Extras/DSL/Internal/Language.hs view
@@ -22,7 +22,7 @@ -- | A synonym for 'Free' DSL. ---type CypherDSL a = Free Expr ()+type CypherDSL a = Free Expr a -- | Prepare 'CREATE' query --
src/Database/Bolt/Extras/DSL/Internal/Types.hs view
@@ -42,6 +42,7 @@ withIdentifier :: Text -> a -> a withLabel :: Text -> a -> a withProp :: (Text, Value) -> a -> a+ withParam :: (Text, Text) -> a -> a -- | Selector for 'Node's. --@@ -54,6 +55,7 @@ data NodeSelector = NodeSelector { nodeIdentifier :: Maybe Text , nodeLabels :: [Text] , nodeProperties :: [(Text, Value)]+ , nodeParams :: [(Text, Text)] } deriving (Show, Eq) @@ -63,6 +65,7 @@ data RelSelector = RelSelector { relIdentifier :: Maybe Text , relLabel :: Text , relProperties :: [(Text, Value)]+ , relParams :: [(Text, Text)] } deriving (Show, Eq) @@ -154,7 +157,7 @@ -- | Empty 'NodeSelector'. defaultNode :: NodeSelector-defaultNode = NodeSelector Nothing [] []+defaultNode = NodeSelector Nothing [] [] [] -- | Shorter synonym for 'defaultRel'. defN :: NodeSelector@@ -162,7 +165,7 @@ -- | Empty 'RelSelector'. defaultRel :: RelSelector-defaultRel = RelSelector Nothing "" []+defaultRel = RelSelector Nothing "" [] [] -- | Shorter synonym for 'defaultRel'. defR :: RelSelector
src/Database/Bolt/Extras/DSL/Typed.hs view
@@ -23,6 +23,7 @@ , lbl , prop , propMaybe+ , param , (=:) , NodeSelector, RelSelector , nodeSelector, relSelector@@ -39,17 +40,29 @@ , (-:) , (<-:) , p++ -- * Queries with parameters+ --+ -- $params++ , CypherDSLParams(..)+ , queryWithParams++ -- ** Implementation details+ , QueryWithParams(..) ) where import Database.Bolt.Extras.DSL.Typed.Instances () import Database.Bolt.Extras.DSL.Typed.Types+import Database.Bolt.Extras.DSL.Typed.Parameters {- $setup >>> :set -XDeriveGeneric >>> :set -XTypeApplications >>> :set -XOverloadedLabels >>> :set -XOverloadedStrings+>>> :set -XDataKinds >>> :load Database.Bolt.Extras.Graph Database.Bolt.Extras.DSL.Typed Database.Bolt.Extras.DSL >>> import Database.Bolt.Extras.DSL.Typed >>> import Data.Text (Text, unpack)@@ -87,6 +100,8 @@ - 'lbl' adds a label represented by some Haskell type - 'prop' adds a new property, making sure that this property exists in one of the labels and has correct type+- 'param' adds a new property with named parameter (@$foo@ syntax in Cypher), making sure that+ this property exists in one of the labels Typically selectors are chained by '.&' starting from 'defN' or 'defR' like this: @@ -117,7 +132,7 @@ ==== Complex queries -These selectors are fully compatible with the 'Database.Bolt.Extras.DSL.DSL':+These selectors are fully compatible with the "Database.Bolt.Extras.DSL": >>> :{ toCypherQ $ do@@ -138,12 +153,12 @@ ==== Dropping types -It is possible to convert typed selectors to untyped ones from 'Database.Bolt.Extras.DSL.DSL' using+It is possible to convert typed selectors to untyped ones from "Database.Bolt.Extras.DSL" using 'nodeSelector' and 'relSelector' funcions. ==== Using with Graph api -This module is also interopable with 'Database.Bolt.Extras.Graph.Graph' API. Here is an example+This module is also interopable with "Database.Bolt.Extras.Graph" API. Here is an example of graph query using typed selectors. >>> import Database.Bolt.Extras.Graph@@ -233,7 +248,7 @@ {- $paths -This module is completely interopable with path selectors from 'Database.Bolt.Extras.DSL.DSL' —+This module is completely interopable with path selectors from "Database.Bolt.Extras.DSL" — adding a 'NodeSelector' or 'RelSelector' to path simply drops all type information, converting it into untyped variant. @@ -245,4 +260,25 @@ >>> toCypherP (#binder .& lbl @Binder .& prop (#uuid =: "123") -: defR .& lbl @ELEMENT !->: #el) (binder:Binder{uuid:"123"})-[:ELEMENT]->(el)+-}++{- $params++There is an option to annotate queries ('Database.Bolt.Extras.DSL.CypherDSL') with parameters they accept,+like this:++> fooQ :: CypherDSLParams '[ '("foo", Int), '("bar", Text) ]+> fooQ = CypherDSLParams $ do+> matchF [ PS $ p $ #n .& lbl @Foo .& param (#foo =: "foo") .& param (#bar =: "bar")+> returnF ["n"]++This will render to the following Cypher expression:++> match (n: Foo {foo: $foo, bar: $bar}) return n++To make sure that all parameters are filled, use 'queryWithParams' function:++> records <- queryWithParams fooQ (#foo =: 42) (#bar =: "Hello")++See below for more examples. -}
src/Database/Bolt/Extras/DSL/Typed/Families.hs view
@@ -55,6 +55,12 @@ Assert _ T1 = Any Assert _ k = k +-- | A version of 'Assert' that returns trivial constraint @()@ when argument is not stuck,+-- discarding its actual value.+type family AssertC (err :: Constraint) (a :: k) :: Constraint where+ AssertC _ T1 = Any+ AssertC _ k = ()+ -- | Error text for the case when records do no have the required field. type family NoFieldError (field :: Symbol) (types :: [Type]) :: k where NoFieldError field types
src/Database/Bolt/Extras/DSL/Typed/Instances.hs view
@@ -15,7 +15,7 @@ import Data.Coerce (coerce) import Data.Function ((&)) import Data.Kind (Type)-import Data.Text (pack)+import Data.Text (Text, pack) import GHC.Exts (proxy#) import GHC.Generics (Rep) import GHC.OverloadedLabels (IsLabel (..))@@ -43,6 +43,8 @@ type AddType (types :: [Type]) (typ :: Type) = typ ': types type HasField (types :: [Type]) (field :: Symbol) (typ :: Type) = Assert (NoFieldError field types) (GetTypeFromList field types) ~ typ+ type HasField' (types :: [Type]) (field :: Symbol) =+ AssertC (NoFieldError field types) (GetTypeFromList field types) withIdentifier = coerce $ UT.withIdentifier @UT.NodeSelector withLabel@@ -58,6 +60,11 @@ => (SymbolS field, typ) -> NodeSelector types -> NodeSelector types withProp (SymbolS field, val) = coerce $ UT.withProp @UT.NodeSelector $ pack field B.=: val + withParam+ :: forall (field :: Symbol) (types :: [Type])+ . (SymbolS field, Text) -> NodeSelector types -> NodeSelector types+ withParam (SymbolS field, name) = coerce $ UT.withParam @UT.NodeSelector (pack field, name)+ instance SelectorLike RelSelector where type CanAddType 'Nothing = () type CanAddType ('Just a)@@ -74,6 +81,13 @@ ) type HasField ('Just record) (field :: Symbol) (typ :: Type) = Assert (NoFieldError field '[record]) (GetTypeFromRecord field (Rep record)) ~ typ+ type HasField' 'Nothing (field :: Symbol)+ = TypeError+ ('Text "Tried to set property " ':<>: 'ShowType field+ ':<>: 'Text " on a relationship without label!"+ )+ type HasField' ('Just record) (field :: Symbol) =+ Assert (NoFieldError field '[record]) (RecordHasField field (Rep record)) ~ 'True withIdentifier = coerce $ UT.withIdentifier @UT.RelSelector withLabel@@ -89,3 +103,8 @@ . B.IsValue typ => (SymbolS field, typ) -> RelSelector types -> RelSelector types withProp (SymbolS field, val) = coerce $ UT.withProp @UT.RelSelector $ pack field B.=: val++ withParam+ :: forall (field :: Symbol) (types :: Maybe Type)+ . (SymbolS field, Text) -> RelSelector types -> RelSelector types+ withParam (SymbolS field, name) = coerce $ UT.withParam @UT.RelSelector (pack field, name)
+ src/Database/Bolt/Extras/DSL/Typed/Parameters.hs view
@@ -0,0 +1,97 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++module Database.Bolt.Extras.DSL.Typed.Parameters+ where++import Control.Monad.IO.Class (MonadIO)+import Data.Kind (Type)+import qualified Data.Map.Strict as Map+import Data.Text (Text, pack)+import Database.Bolt (BoltActionT,+ IsValue (..),+ Record, Value,+ queryP)+import GHC.TypeLits (Symbol)++import Database.Bolt.Extras.DSL.Internal.Executer (formQuery)+import Database.Bolt.Extras.DSL.Internal.Language (CypherDSL)+import Database.Bolt.Extras.DSL.Typed.Types (SymbolS (..))++{- $setup+>>> :set -XTypeApplications+>>> :set -XOverloadedLabels+>>> :set -XDataKinds+>>> :set -XOverloadedStrings+>>> :load Database.Bolt.Extras.DSL.Typed.Instances Database.Bolt.Extras.DSL.Typed.Parameters+>>> import Control.Monad.IO.Class+>>> import Database.Bolt (BoltActionT, Record)+>>> import Database.Bolt.Extras.DSL (returnF)+>>> import Database.Bolt.Extras.DSL.Typed.Parameters+-}++-- | A wrapper around arbitrary 'CypherDSL' expression which stores type-level list of named+-- parameters (@$foo@) with their types.+newtype CypherDSLParams (params :: [(Symbol, Type)]) (a :: Type)+ = CypherDSLParams (CypherDSL a)++-- | This type class ensures safety of queries with parameters by checking in compile time that+-- all parameters are supplied and have correct type.+--+-- Instances of this class will add more arguments to @fun@, one for each element in @params@.+--+-- This should be considered an implementation detail.+class QueryWithParams (params :: [(Symbol, Type)]) (m :: Type -> Type) fun | params m -> fun where+ -- | Internal function that accumulates parameters from type-level list.+ collectParams :: CypherDSL () -> [(Text, Value)] -> fun++-- | Base case: if there are no parameters, perform query with 'queryP'.+instance MonadIO m => QueryWithParams '[] m (BoltActionT m [Record]) where+ collectParams dsl params = queryP (formQuery dsl) $ Map.fromList params++-- | Recursion case: append the next parameter to accumulator and to function's type.+instance (IsValue typ, QueryWithParams rest m fun)+ => QueryWithParams ('(field, typ) ': rest) m ((SymbolS field, typ) -> fun)+ where++ collectParams dsl params (SymbolS s, a) = collectParams @rest @m dsl ((pack s, toValue a) : params)++-- | Run a query (in the form of 'CypherDSLParams'). This is a function of variable number of arguments.+-- Actual number will be determined by type-level list @params@.+--+-- A couple of examples:+--+-- >>> dsl = CypherDSLParams (returnF []) :: CypherDSLParams '[ '("foo", Int), '("bar", Text) ] ()+-- >>> :t queryWithParams dsl+-- queryWithParams dsl+-- :: MonadIO m =>+-- (SymbolS "foo", Int)+-- -> (SymbolS "bar", Text) -> BoltActionT m [Record]+-- >>> :t queryWithParams dsl (#foo =: 42)+-- queryWithParams dsl (#foo =: 42)+-- :: MonadIO m => (SymbolS "bar", Text) -> BoltActionT m [Record]+-- >>> :t queryWithParams dsl (#foo =: 42) (#bar =: "Hello")+-- queryWithParams dsl (#foo =: 42) (#bar =: "Hello")+-- :: MonadIO m => BoltActionT m [Record]+-- >>> :t queryWithParams dsl (#foo =: True)+-- ...+-- ... Couldn't match type ‘Int’ with ‘Bool’+-- ...+-- >>> :t queryWithParams dsl (#bar =: 42)+-- ...+-- ... Couldn't match type ‘"bar"’ with ‘"foo"’+-- ...+queryWithParams+ :: forall params m fun+ . MonadIO m+ => QueryWithParams params m fun+ => CypherDSLParams params ()+ -> fun+queryWithParams (CypherDSLParams dsl) = collectParams @params @m dsl []
src/Database/Bolt/Extras/DSL/Typed/Types.hs view
@@ -47,6 +47,9 @@ -- of labels. type HasField (types :: k) (field :: Symbol) (typ :: Type) :: Constraint + -- | This constraint checks that field with this name exists in the collection, with any type.+ type HasField' (types :: k) (field :: Symbol) :: Constraint+ -- | Set an identifier — Cypher variable name. withIdentifier :: Text -> a types -> a types @@ -65,6 +68,14 @@ -> a types -> a types + -- | Add a property as named parameter (@$foo@). Only checks that given property exists,+ -- no matter its type.+ withParam+ :: HasField' types field+ => (SymbolS field, Text)+ -> a types+ -> a types+ -- | Synonym for 'withLabel' with label type variable as first one, enabling @lbl \@Foo@ type -- application syntax. lbl@@ -113,6 +124,28 @@ -> a types -> a types propMaybe (name, Just val) = withProp (name, val) propMaybe _ = id++-- | Shorter synonym for 'withParam'.+--+-- >>> data Foo = Foo { foo :: Int, bar :: Maybe String } deriving Generic+-- >>> toCypherN $ defN .& lbl @Foo .& param (#foo =: "foo")+-- (:Foo{foo:$foo})+-- >>> toCypherN $ defN .& lbl @Foo .& prop (#foo =: 42) .& param (#bar =: "bar")+-- (:Foo{foo:42,bar:$bar})+-- >>> toCypherN $ defN .& lbl @Foo .& param (#baz =: "baz")+-- ...+-- ... There is no field "baz" in any of the records+-- ... '[Foo]+-- ...+--+-- __NOTE__: this will add @$@ symbol to parameter name automatically.+param+ :: forall (field :: Symbol) (a :: k -> Type) (types :: k)+ . SelectorLike a+ => HasField' types field+ => (SymbolS field, Text)+ -> a types -> a types+param = withParam -- | Smart constructor for a pair of field name and its value. To be used with @OverloadedLabels@: --
src/Database/Bolt/Extras/Internal/Cypher.hs view
@@ -59,8 +59,13 @@ instance ToCypher Property where toCypher (propTitle, value) = T.concat [propTitle, pack ":", toCypher value] +instance ToCypher (Text, Text) where+ toCypher (propTitle, param) = propTitle <> ":$" <> param+ -- | Several properties are formatted with concatenation. -- instance ToCypher [Property] where toCypher = T.intercalate "," . map toCypher +instance ToCypher [(Text, Text)] where+ toCypher = T.intercalate "," . map toCypher
test/Doctest.hs view
@@ -10,4 +10,5 @@ [ "-isrc" , "src/Database/Bolt/Extras/DSL/Typed.hs" , "src/Database/Bolt/Extras/DSL/Typed/Types.hs"+ , "src/Database/Bolt/Extras/DSL/Typed/Parameters.hs" ]