{-# LANGUAGE ExplicitNamespaces #-} {- | Type-safe DSL for Cypher This module extends selectors from 'Database.Bolt.Extras.DSL.DSL' with extra type-level information to make them more type-safe to use. None of additional type information exists at runtime, so using this module does not degrade performance at all. -} module Database.Bolt.Extras.DSL.Typed ( -- * Selecting Nodes and Relations -- -- $selecting -- ** Type safety -- -- $safety SelectorLike(..) , LabelConstraint , lbl , prop , propMaybe , param , type (=:) , (=:) , NodeSelector, RelSelector , nodeSelector, relSelector , defN , defR -- * Building paths -- -- $paths , (.&) , (!->:) , (!-:) , (-:) , (<-:) , p -- * Queries with parameters -- -- $params , CypherDSLParams(..) , queryWithParams -- ** Implementation details , QueryWithParams(..) ) where import Database.Bolt.Extras.DSL.Typed.Instances () import Database.Bolt.Extras.DSL.Typed.Parameters import Database.Bolt.Extras.DSL.Typed.Types {- $setup >>> :set -XDeriveGeneric >>> :set -XTypeApplications >>> :set -XOverloadedLabels >>> :set -XOverloadedStrings >>> :set -XDataKinds >>> :set -XTypeOperators >>> :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) >>> import GHC.Generics (Generic) >>> import Database.Bolt.Extras (toCypher) >>> import Data.Function ((&)) >>> import qualified Database.Bolt.Extras.Graph as G >>> toCypherN = putStrLn . unpack . toCypher . nodeSelector >>> toCypherR = putStrLn . unpack . toCypher . relSelector >>> toCypherP = putStrLn . unpack . toCypher >>> data Binder = Binder { uuid :: Text } deriving (Generic) >>> data Foo = Foo { bar :: Bool, foo :: Int } deriving (Generic) >>> data PLACE = PLACE deriving (Generic) >>> data ELEMENT = ELEMENT deriving (Generic) >>> data Name = Name { name :: Text } deriving (Generic) >>> data User = User { user :: Text } deriving (Generic) >>> data NAME_OF = NAME_OF deriving (Generic) >>> data USER_CREATED = USER_CREATED { timestamp :: Int } deriving (Generic) >>> data Library = Library deriving (Generic) >>> data BinderLibrary = BinderLibrary deriving (Generic) >>> import Database.Bolt.Extras.DSL as DSL (createF, mergeF, Selector(..), formQuery, returnF) >>> toCypherQ = putStrLn . unpack . DSL.formQuery >>> formQueryG = putStrLn . unpack . G.formQuery @G.GetRequest [] -} {- $selecting There are types for Node and Relationship selectors: 'NodeSelector' and 'RelSelector'. Both of them carry extra type-level information about labels assigned to Cypher variables. Empty selectors may be constructed with 'defN' and 'defR' respectively. Selectors can be extended with the following combinators: - 'withIdentifier' adds an identifier (variable name) - '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: >>> toCypherN $ defN .& withIdentifier "binder" .& lbl @Binder .& prop (#uuid =: "123-456") (binder:Binder{uuid:"123-456"}) Alternatively, @OverloadedLabels@ may be used to create an empty selector with an identifier: >>> toCypherN $ #binder .& lbl @Binder .& prop (#uuid =: "123-456") (binder:Binder{uuid:"123-456"}) This syntax is more concise and makes it obvious what is going on. Thus, it is the preferred one. The type used with 'lbl' should have 'GHC.Generics.Generic' instance. Nodes may have multiple labels: >>> toCypherN $ defN .& lbl @Binder .& lbl @Foo (:Foo:Binder) But relations have at most one: >>> defR .& lbl @PLACE .& lbl @ELEMENT ... ... Can't add a new label to relationship selector that already has label PLACE! ... ==== Complex queries These selectors are fully compatible with the "Database.Bolt.Extras.DSL": >>> :{ toCypherQ $ do mergeF [ PS $ p $ #name .& lbl @Name .& prop (#name =: "CT42") ] mergeF [ PS $ p $ #user .& lbl @User .& prop (#user =: "123-456") ] createF [ PS $ p $ #lib .& lbl @Library .& lbl @BinderLibrary , PS $ #name -: defR .& lbl @NAME_OF !->: #lib , PS $ #user -: defR .& lbl @USER_CREATED .& prop (#timestamp =: 1572340394000) !->: #lib ] returnF ["lib"] :} MERGE (name:Name{name:"CT42"}) MERGE (user:User{user:"123-456"}) CREATE (lib:BinderLibrary:Library), (name)-[:NAME_OF]->(lib), (user)-[:USER_CREATED{timestamp:1572340394000}]->(lib) RETURN lib ==== Dropping types 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" API. Here is an example of graph query using typed selectors. >>> import Database.Bolt.Extras.Graph >>> nToG = ngFromDSL . nodeSelector >>> rToG = rgFromDSL . relSelector >>> :{ formQueryG $ emptyGraph & addNode "binder" (nToG (defN .& lbl @Binder .& prop (#uuid =: "123-456")) & isReturned & withReturn allProps ) & addNode "user" (nToG (defN .& lbl @User .& prop (#user =: "098-765")) & isReturned & withReturn allProps ) & addRelation "user" "binder" (rToG (defR .& lbl @USER_CREATED) & isReturned & withReturn allProps ) :} MATCH (user)-[user0binder :USER_CREATED {}]->(binder) , (binder :Binder {uuid:"123-456"}) , (user :User {user:"098-765"}) WITH DISTINCT binder, user, user0binder RETURN { id: id(binder), labels: labels(binder), props: properties(binder) } as binder , { id: id(user), labels: labels(user), props: properties(user) } as user , { id: id(user0binder), label: type(user0binder), props: properties(user0binder) } as user0binder -} {- $safety Obviosuly, if you try to use @lbl \@Foo@ syntax with undefined type @Foo@, GHC itself will report the error. Here are more interesting cases: >>> -- Properties are looked for in all labels >>> toCypherN $ defN .& lbl @Binder .& lbl @Foo .& prop (#foo =: 42) .& prop (#uuid =: "123-456") (:Foo:Binder{uuid:"123-456",foo:42}) >>> -- Adding a property to node without any labels >>> defN .& prop (#uuid =: "123-456") ... ... There is no field "uuid" in any of the records ... '[] ... >>> -- Adding a property that does not exist in the label >>> defN .& lbl @Binder .& prop (#foo =: 42) ... ... There is no field "foo" in any of the records ... '[Binder] ... >>> -- Adding a property with wrong type >>> defN .& lbl @Binder .& prop (#uuid =: 42) ... ... No instance for (Num Text) arising from the literal ‘42’ ... Here we see that GHC undestands that the property should have type @Text@ and tries to unify it with the type of literal @42@, which is @Num a => a@. >>> -- Adding a property to relationship without a label >>> defR .& prop (#foo =: 42) ... ... Tried to set property "foo" on a relationship without label! ... -} {- $paths 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. Due to limitation of what symbols are allowed in operators and operator-like data constructors, this module renames some of the path constructors. Precedence of the operators allow them to be combined in the same expression with '.&' and '$' without any extra parentheses. Here is an example of a path constructed this way: >>> 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. -} {- $tests These should not generate compile errors. >>> data TestRecord = TestRecord { first :: Text, second :: Int, third :: Bool, fourth :: Maybe String, fifth :: Text } deriving (Eq, Show, Generic) >>> defN .& lbl @TestRecord .& prop (#first =: "foo") ... NodeSelector ... >>> defN .& lbl @TestRecord .& prop (#third =: True) .& prop (#fifth =: "lalala") ... NodeSelector ... >>> defN .& lbl @TestRecord .& prop (#fourth =: "123") .& prop (#first =: "noes") ... NodeSelector ... -}