Packages

A DSL to interact with cypher query language

Current section

Files

Jump to
ex_cypher lib ex_cypher.ex
Raw

lib/ex_cypher.ex

defmodule ExCypher do
@moduledoc """
A DSL to make it easy to integrate other apps to Neo4J applications.
This module provide a clean API to build queries using CYPHER query language,
which is largely used by Neo4j
## Usage:
First of all, add `import ExCpyher` to the top of your module, so that
`ExCypher.cypher/0` macro is available to your functions.
After that, you can use cypher-friendly functions to build your queries:
iex> cypher do: match(node(:n))
"MATCH (n)"
iex> cypher do: match(node(:p, [:Person]))
"MATCH (p:Person)"
iex> cypher do
...> match(node(:p, [:Person], %{name: "bob"}))
...> end
~S[MATCH (p:Person {name:"bob"})]
Also we support a familiar syntax to build nodes associations, by using either
`:--`, `:->` or `:<-` to denote the nodes associations, as follows:
iex> cypher do
...> match node(:p, [:Person]) -- node(:c, [:Company])
...> end
"MATCH (p:Person)--(c:Company)"
iex> cypher do
...> match node(:p, [:Person]) -- rel(:WORKS_IN) -- node(:c, [:Company])
...> return :p
...> end
"MATCH (p:Person)-[WORKS_IN]-(c:Company) RETURN p"
iex> cypher do
...> match (node(:p, [:Person]) -- rel(:WORKS_IN) -> node(:c, [:Company]))
...> end
"MATCH (p:Person)-[WORKS_IN]->(c:Company)"
iex> cypher do
...> match (node(:c, [:Company]) <- rel(:WORKS_IN) -- node(:p, [:Person]))
...> end
"MATCH (c:Company)<-[WORKS_IN]-(p:Person)"
See `ExCypher.Node.node/0` and `ExCypher.Relationship.rel/0` for more
information on how to build your nodes and relationships.
If you need to order your returned nodes, you can use both `order` and `limit`:
iex> cypher do
...> match node(:s, [:Sharks])
...> order s.name
...> limit 10
...> return s.name, s.population
...> end
"MATCH (s:Sharks) ORDER BY s.name LIMIT 10 RETURN s.name, s.population"
Also, you can choose the ordering direction of your matched nodes with a
tuple syntax:
iex> cypher do
...> match node(:s, [:Sharks])
...> order {s.name, :asc}, {s.age, :desc}
...> return :s
...> end
"MATCH (s:Sharks) ORDER BY s.name ASC, s.age DESC RETURN s"
There're also support for `create` and `merge` statements from cypher
language:
iex> cypher do
...> create node(:p, [:Player], %{nick: "like4boss", score: 100})
...> return p.name
...> end
~S[CREATE (p:Player {nick:"like4boss",score:100}) RETURN p.name]
iex> cypher do
...> create (node(:c, [:Country], %{name: "Brazil"}) -- rel([:HAS_CITY]) -> node([:City], %{name: "São Paulo"}))
...> return :c
...> end
~S|CREATE (c:Country {name:"Brazil"})-[:HAS_CITY]->(:City {name:"São Paulo"}) RETURN c|
iex> cypher do
...> merge node(:p, [:Player], %{nick: "like4boss"})
...> merge node(:p2, [:Player], %{nick: "marioboss"})
...> return p.name
...> end
~S|MERGE (p:Player {nick:"like4boss"}) MERGE (p2:Player {nick:"marioboss"}) RETURN p.name|
iex> cypher do
...> merge node(:p, [:Player], %{nick: "like4boss"})
...> merge node(:p2, [:Player], %{nick: "marioboss"})
...> merge (node(:p) -- rel([:IN_LOBBY]) -> node(:p2))
...> return p.name
...> end
~S|MERGE (p:Player {nick:"like4boss"}) MERGE (p2:Player {nick:"marioboss"}) MERGE (p)-[:IN_LOBBY]->(p2) RETURN p.name|
## Caveats with complex relationships
Each of the presented elements above are independent functions that work
together to build complex cypher queries under the hood. When building complex
association between nodes and relationships, you must be aware that in some
cases you may end fighting agaisnt elixir compiler when using the `:->`
operator, which is reserved to functions and pattern matching.
This can be solved with a simple caveat specifying a little better the scope
of your association. Before diving into this, consider this example:
iex> cypher do
...> create node(:p, [:Player], %{name: "mario"}),
...> node(:p2, [:Player], %{name: "luigi"})
...> end
~S|CREATE (p:Player {name:"mario"}), (p2:Player {name:"luigi"})|
Each node defined in this query act as an argument to a bigger function.
If you want to use an association in this query, you may try the following:
```
cypher do
create node(:p, [:Player], %{name: "mario"}),
node(:p2, [:Player], %{name: "luigi"}),
node(:p) -- rel([:IS_FRIEND]) -> node(:p2)
end
```
But this will result in a compilation error. Instead, let's take care about
the operator precedence here and wrap the entire association in parenthesis,
this way resolving any conflict made by the compiler:
iex> cypher do
...> create node(:p, [:Player], %{name: "mario"}),
...> node(:p2, [:Player], %{name: "luigi"}),
...> (node(:p) -- rel([:IS_FRIEND]) -> node(:p2))
...> end
~S|CREATE (p:Player {name:"mario"}), (p2:Player {name:"luigi"}), (p)-[:IS_FRIEND]->(p2)|
"""
alias ExCypher.{Buffer, Statement}
import ExCypher.Clause, only: [is_supported: 1]
@doc """
Wraps contents of a Cypher query and returns the query string.
"""
defmacro cypher(do: block) do
cypher_query(block, __CALLER__)
end
defp cypher_query(block, env) do
{:ok, pid} = Buffer.new_query()
Macro.postwalk(block, fn
{command, _ctx, args} when is_supported(command) ->
params = Statement.parse(command, args, env)
Buffer.put_buffer(pid, params)
term ->
term
end)
query = Buffer.generate_query(pid)
Buffer.stop_buffer(pid)
quote do
unquote(query)
|> Enum.reverse()
|> List.flatten()
|> Enum.join(" ")
|> String.replace(" , ", ", ")
end
end
end