Packages

Neo4j driver for Elixir, using the fast Bolt protocol

Current section

Files

Jump to
boltx lib bolt_sips query.ex
Raw

lib/bolt_sips/query.ex

defmodule Bolt.Sips.Query do
@moduledoc """
Provides a simple Query DSL.
You can run simple Cypher queries with or w/o parameters, for example:
{:ok, row} = Bolt.Sips.query(conn, "match (n:Person {bolt_sips: true}) return n.name as Name limit 5")
assert List.first(row)["Name"] == "Patrick Rothfuss"
Or more complex ones:
cypher = \"""
MATCH (p:Person {bolt_sips: true})
RETURN p, p.name AS name, upper(p.name) as NAME,
coalesce(p.nickname,"n/a") AS nickname,
{ name: p.name, label:head(labels(p))} AS person
\"""
{:ok, r} = Bolt.Sips.query(conn, cypher)
As you can see, you can organize your longer queries using the Elixir multiple line conventions, for readability.
And there is one more trick, you can use for more complex Cypher commands: use `;` as a transactional separator.
For example, say you want to clean up the test database **before** creating some tests entities. You can do that like this:
cypher = \"""
MATCH (n {bolt_sips: true}) OPTIONAL MATCH (n)-[r]-() DELETE n,r;
CREATE (BoltSips:BoltSips {title:'Elixir sipping from Neo4j, using Bolt', released:2016, license:'MIT', bolt_sips: true})
CREATE (TNOTW:Book {title:'The Name of the Wind', released:2007, genre:'fantasy', bolt_sips: true})
CREATE (Patrick:Person {name:'Patrick Rothfuss', bolt_sips: true})
CREATE (Kvothe:Person {name:'Kote', bolt_sips: true})
CREATE (Denna:Person {name:'Denna', bolt_sips: true})
CREATE (Chandrian:Deamon {name:'Chandrian', bolt_sips: true})
CREATE
(Kvothe)-[:ACTED_IN {roles:['sword fighter', 'magician', 'musician']}]->(TNOTW),
(Denna)-[:ACTED_IN {roles:['many talents']}]->(TNOTW),
(Chandrian)-[:ACTED_IN {roles:['killer']}]->(TNOTW),
(Patrick)-[:WROTE]->(TNOTW)
\"""
assert {:ok, _r} = Bolt.Sips.query(conn, cypher)
In the example above, this command: `MATCH (n {bolt_sips: true}) OPTIONAL MATCH (n)-[r]-() DELETE n,r;` will be executed in a distinct transaction, before all the other queries
See the various tests, or more examples and implementation details.
"""
alias Bolt.Sips.{QueryStatement, Response, Types, Error, Exception}
@cypher_seps ~r/;(.){0,1}\n/
@spec query!(Bolt.Sips.conn(), String.t()) :: Response.t() | Exception.t()
def query!(conn, statement), do: query!(conn, statement, %{})
@spec query!(Bolt.Sips.conn(), String.t(), map, Keyword.t()) :: Response.t() | Exception.t()
def query!(conn, statement, params, opts \\ []) when is_map(params) do
with {:ok, r} <- query_commit(conn, statement, params, opts) do
r
else
{:error, msg} ->
raise Exception, message: msg
e ->
raise Exception, message: "unexpected error: #{inspect(e)}"
end
end
@spec query(Bolt.Sips.conn(), String.t()) :: {:error, Error.t()} | {:ok, Response.t()}
def query(conn, statement), do: query(conn, statement, %{})
@spec query(Bolt.Sips.conn(), String.t(), map, Keyword.t()) ::
{:error, Error.t()} | {:ok, Response.t()}
def query(conn, statement, params, opts \\ []) when is_map(params) do
case query_commit(conn, statement, params, opts) do
{:error, message} -> {:error, %Error{message: message}}
r -> r
end
rescue
e in Bolt.Sips.Exception ->
{:error, %Bolt.Sips.Error{code: e.code, message: e.message}}
end
###########
# Private #
###########
@spec query_commit(Bolt.Sips.conn(), String.t(), map, Keyword.t()) ::
{:error, Error.t()} | {:ok, Response.t()}
defp query_commit(conn, statement, params, opts) do
statements =
statement
|> String.split(@cypher_seps, trim: true)
|> Enum.map(&String.trim/1)
|> Enum.filter(&(String.length(&1) > 0))
formatted_params =
params
|> Enum.map(&format_param/1)
|> Enum.map(fn {k, {:ok, value}} -> {k, value} end)
|> Map.new()
errors =
formatted_params
|> Enum.filter(fn {_, formated} ->
case formated do
{:error, _} -> true
_ -> false
end
end)
|> Enum.map(fn {k, {:error, error}} -> {k, error} end)
{:ok, commit!(errors, conn, statements, formatted_params, opts)}
rescue
e in [RuntimeError, DBConnection.ConnectionError] ->
{:error, Bolt.Sips.Error.new(e.message)}
e in Exception ->
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
# n/a in newer Elixir version:
# reraise e, __STACKTRACE__
# using a safe call, for backward compatibility
reraise e, stacktrace
e ->
{:error, e}
end
defp commit!([], conn, statements, formatted_params, opts),
do: tx!(conn, statements, formatted_params, opts)
defp commit!(errors, _conn, _statements, _formatted_params, _opts),
do: raise(Exception, message: "Unable to format params: #{inspect(errors)}")
defp tx!(conn, [statement], params, opts), do: hd(send!(conn, statement, params, opts))
# todo It returns [Response.t] !!!
defp tx!(conn, statements, params, opts) when is_list(statements),
do: Enum.reduce(statements, [], &send!(conn, &1, params, opts, &2))
defp send!(conn, statement, params, opts, acc \\ [])
defp send!(conn, statement, params, opts, acc) do
# Retrieve timeout defined in config
prefix = Keyword.get(opts, :prefix, :default)
conf_timeout =
Bolt.Sips.info()
|> Map.get(prefix)
|> Map.get(:user_options)
|> Keyword.get(:timeout)
opts = Keyword.put_new(opts, :timeout, Keyword.get(opts, :timeout, conf_timeout))
case DBConnection.execute(conn, %QueryStatement{statement: statement}, params, opts) do
{:ok, _query, resp} ->
case Response.transform(resp) do
{:ok, %Response{} = r} -> acc ++ [r]
{:error, e} -> raise DBConnection.ConnectionError, message: e
end
{:error, %Bolt.Sips.Internals.Error{code: code, message: msg}} ->
raise Exception, code: code, message: msg
{:error, %{message: message}} ->
raise DBConnection.ConnectionError, message: message
end
end
# Format the param to be used in query
# must return a tuple of the form: {:ok, param} or {:error, param}
# In order to let query_commit handle the error
@spec format_param({String.t(), any()}) :: {String.t(), {:ok | :error, any()}}
defp format_param({name, %Types.Duration{} = duration}),
do: {name, Types.Duration.format_param(duration)}
defp format_param({name, %Types.Point{} = point}), do: {name, Types.Point.format_param(point)}
defp format_param({name, value}), do: {name, {:ok, value}}
end