defmodule RedisGraph.Query do
@moduledoc """
Query module provides functions to build the
cypther query for RedisGraph database.
The module exposes functions that represent
entities (Node/Relationship) and [Cypher clauses]
(https://redis.io/docs/stack/graph/cypher_support/#clauses)
(MATCH/WHERE/RETURN etc.) through which client can build
the desired query and pass it to `RedisGraph.query/3` to
interact with the database. Query structure holds the context,
that contains data necessary when building the actual query.
The context shouldn't be altered by the client directly,
instead only public functions from this module should be,
which would internally change it.
The `RedisGraph.Query` supports the following Cypher clauses through functions:
- CREATE: `create/1`
- MATCH: `match/1`
- OPTIONALMATCH`: `optional_match/1`
- MERGE: `merge/1`
- DELETE: `delete/2`
- WHERE: `where/5`, `where_not/5`, `or_where/5`, `and_where/5`, `xor_where/5`, `or_not_where/5`, `and_not_where/5`, `xor_not_where/5`
- ORDERBY`: `order_by/3`
- SET: `set/4`, `set_property/5`
- ONMATCH SET`: `on_match_set/4`, `on_match_set_property/5`
- ONCREATE SET`: `on_create_set/4`, `on_create_set_property/5`
- WITH: `with/3`, `with_property/4`, `with_function/4`, `with_function_and_property/5`
- LIMIT: `limit/2`
- `SKIP`: `skip/2`
- `RETURN`: `return/3`, `return_property/4`, `return_function/4`, `return_function_and_property/5`
- `RETURN DISTINCT`: `return_distinct/3`, `return_distinct_property/4`, `return_distinct_function/4`, `return_distinct_function_and_property/5`
Node entity is supported through functions: `node/3`, `node/4`
Relatioshipentity is supported through functions: `relationship_from_to/3`, `relationship_from_to/4`, `relationship_to_from/3`, `relationship_to_from/4`
After building the query, you will end up with either `{:ok, query_message}` or `{:error, error_message}`.
The workflow of using the `RedisGraphQuery` module is the following:
- create anew query using `new/0`
- populate the query through `match/1`, `node/3`, `return/3` etc.
- receive the query string by building it through `build_query/1`
## Examples
```
# Creating a valid query
{:ok, query} =
Query.new()
|> Query.match()
|> Query.node(:n, ["Person"], %{age: 30, name: "John Doe", works: true})
|> Query.relationship_from_to(:r, "TRAVELS_TO", %{purpose: "pleasure"})
|> Query.node(:m, ["Place"], %{name: "Japan"})
|> Query.return(:n)
|> Query.return_property(:n, "age", :Age)
|> Query.return(:m)
|> Query.build_query()
# query will hold
# "MATCH "MATCH (n:Person {age: 30, name: 'John Doe', works: true})-[r:TRAVELS_TO {purpose: 'pleasure'}]->(m:Place {name: 'Japan'}) RETURN n, n.age AS Age, m"
# Creating an invalid query
{:error, error} =
Query.new() |> Query.match() |> Query.node(:n, ["Person"], %{age: 30, name: "John Doe", works: true}) |> Query.relationship_from_to(:r, "TRAVELS_TO", %{purpose: "pleasure"}) |> Query.node(:m, ["Place"], %{name: "Japan"}) |> Query.build_query()
# error will hold
# "In case you provide MATCH, OPTIONAL MATCH - then RETURN, RETURN DISCTINCT or DELETE also has to be provided. E.g. new() |> match |> node(:n) |> return(:n)"
```
You will specify the node through node() and relationship through either relationship_from_to() or relationship_to_from().
```
{:ok, query} = Query.new()
|> Query.create()
|> Query.node(:n, ["Person"])
|> Query.relationship_from_to(:r, "TRAVELS_TO")
|> Query.node(:m, ["City"])
|> Query.relationship_from_to(:t, "IN")
|> Query.node(:b, ["Country"])
|> Query.relationship_to_from(:y, "HAS")
|> Query.node(:v, ["Emperor"])
|> Query.build_query()
# query would hold
# "CREATE (n:Person)-[r:TRAVELS_TO]->(m:City)-[t:IN]->(b:Country)<-[y:HAS]-(v:Emperor)"
```
"""
alias RedisGraph.{Node, Relationship, Util}
@opaque t() :: %__MODULE__{
current_clause: clauses() | nil,
last_element: RedisGraph.Node.t() | RedisGraph.Relationship.t() | nil,
error: String.t() | nil,
nodes: %{atom() => RedisGraph.Node.t()},
relationships: %{atom() => RedisGraph.Relationship.t()},
variables: [String.t()],
used_clauses: [[atom()]] | [],
used_clauses_with_data: [[map()]] | []
}
@typep clauses() ::
:create
| :match
| :optional_match
| :merge
| :delete
| :set
| :on_match_set
| :on_create_set
| :with
| :where
| :order_by
| :limit
| :skip
| :return
@typep accepted_operator_in_where_clause() ::
:equals
| :not_equal
| :bigger
| :bigger_or_equal
| :smaller
| :smaller_or_equal
| :starts_with
| :ends_with
| :contains
| :in
@typep accepted_value() :: String.t() | number() | boolean() | nil | list() | map()
@accepted_operator_to_string_in_where_clause %{
equals: "=",
not_equal: "<>",
bigger: ">",
bigger_or_equal: ">=",
smaller: "<",
smaller_or_equal: "<=",
starts_with: "STARTS WITH",
ends_with: "ENDS WITH",
contains: "CONTAINS",
in: "IN",
is: "IS",
is_not: "IS NOT"
}
@accepted_operator_for_number_in_where_clause [
:equals,
:not_equal,
:bigger,
:bigger_or_equal,
:smaller,
:smaller_or_equal
]
@accepted_operator_for_nil_in_where_clause [:is, :is_not]
@accepted_operator_for_binary_in_where_clause [
:equals,
:not_equal,
:bigger,
:bigger_or_equal,
:smaller,
:smaller_or_equal,
:starts_with,
:ends_with,
:contains
]
@accepted_operator_for_boolean_in_where_clause [:equals]
@accepted_operator_for_list_in_where_clause [:in]
defstruct [
:current_clause,
:last_element,
error: nil,
nodes: %{},
relationships: %{},
variables: [],
used_clauses: [],
used_clauses_with_data: []
]
@doc """
Used to initialize the `RedisGraph.Query` structure and create the query.
Funciton returns the query context, whose contents should be updated using
subsequent functions in this module (e.g. `match/1`, `return/3`) and not manipulated directly.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n) RETURN n"
```
"""
@spec new() :: t()
def new() do
struct(__MODULE__)
end
@doc """
Add `MATCH` clause into the context and receive the updated context.
Receives the context which is provided from `new/0` function.
After `match/1` provide the entities which you want to match using
`node/2`, `relationship_from_to/2`, `relationship_to_from/2` functions.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n, ["Person"], %{age: 30, name: "John"}) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n:Person {age: 30, name: 'John'}) RETURN n"
```
"""
@spec match(t()) :: t()
def match(%{error: nil} = context) do
context = check_if_provided_context_has_correct_structure(context)
context = add_clause_if_not_present(context, :match)
context = check_if_return_clause_already_provided(context, :match)
%{error: error} = context
case error do
nil -> Map.put(context, :current_clause, :match)
_ -> context
end
end
def match(context) do
check_if_provided_context_has_correct_structure(context)
end
@doc """
Add `OPTIONAL MATCH` clause into the context and receive the updated context.
Receives the context which is provided from `new/0` function.
After `optional_match/1` provide the entities which you want to match using
`node/2`, `relationship_from_to/2`, `relationship_to_from/2` functions.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.optional_match() |> Query.node(:n, ["Person"], %{age: 30, name: "John"}) |> Query.return(:n) |> Query.build_query()
# query will hold
# "OPTIONAL MATCH (n:Person {age: 30, name: 'John'}) RETURN n"
```
"""
@spec optional_match(t()) :: t()
def optional_match(%{error: nil} = context) do
context = check_if_provided_context_has_correct_structure(context)
context = add_clause_if_not_present(context, :optional_match)
context = check_if_return_clause_already_provided(context, :optional_match)
%{error: error} = context
case error do
nil -> Map.put(context, :current_clause, :optional_match)
_ -> context
end
end
def optional_match(context) do
check_if_provided_context_has_correct_structure(context)
end
@doc """
Add `MERGE` clause into the context and receive the updated context.
Receives the context which is provided from `new/0` function.
After `merge/1` provide the entities which you want to match using
`node/2`, `relationship_from_to/2`, `relationship_to_from/2` functions.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.merge() |> Query.node(:n, ["Person"], %{age: 30, name: "John"}) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MERGE (n:Person {age: 30, name: 'John'}) RETURN n"
```
"""
@spec merge(t()) :: t()
def merge(%{error: nil} = context) do
context = check_if_provided_context_has_correct_structure(context)
context = add_clause_if_not_present(context, :merge)
context = check_if_return_clause_already_provided(context, :merge)
%{error: error} = context
case error do
nil -> Map.put(context, :current_clause, :merge)
_ -> context
end
end
def merge(context) do
check_if_provided_context_has_correct_structure(context)
end
@doc """
Add `CREATE` clause into the context and receive the updated context.
Receives the context which is provided from `new/0` function.
After `create/1` provide the entities which you want to match using
`node/2`, `relationship_from_to`, `relationship_to_from/2` functions.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.create() |> Query.node(:n, ["Person"], %{age: 30, name: "John"}) |> Query.return(:n) |> Query.build_query()
# query will hold
# "CREATE (n:Person {age: 30, name: 'John'}) RETURN n"
```
"""
@spec create(t()) :: t()
def create(%{error: nil} = context) do
context = check_if_provided_context_has_correct_structure(context)
context = add_clause_if_not_present(context, :create)
context = check_if_return_clause_already_provided(context, :create)
%{error: error} = context
case error do
nil -> Map.put(context, :current_clause, :create)
_ -> context
end
end
def create(context) do
check_if_provided_context_has_correct_structure(context)
end
@doc """
Add `DELETE` clause into the context and receive the updated context.
Provide the `context` and `alias` (as atom) of the entity you want to delete.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n, ["Person"], %{age: 30, name: "John"}) |> Query.delete(:n) |> Query.build_query()
# query will hold
# "MATCH (n:Person {age: 30, name: 'John'}) DELETE n"
```
If provided entity alias is was not mentioned before, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.create() |> Query.node(:n, ["Person"], %{age: 30, name: "John"}) |> Query.delete(:m) |> Query.build_query()
# error will hold
# "Provided alias: :m was not mentioned before. Pass the alias first: e.g. new() |> match() |> node(:n) |> order_by_property(:n, \"age\") |> ..."
```
"""
@spec delete(t(), atom()) :: t()
def delete(%{error: nil} = context, alias) do
current_clause = Map.get(context, :current_clause)
context = check_if_provided_context_has_correct_structure(context)
context =
if current_clause != :delete do
context = add_clause_if_not_present(context, :delete)
Map.put(context, :current_clause, :delete)
else
context
end
context = check_if_provided_alias_present(context, alias)
context = check_if_match_ends_with_relationship(context)
context = check_if_alias_is_atom(context, alias)
context = check_if_match_or_create_or_merge_clause_provided(context, :delete)
%{error: error} = context
case error do
nil ->
context = update_used_clauses_with_data(context, alias)
context = Map.put(context, :current_clause, :delete)
context
_ ->
context
end
end
def delete(context, _alias) do
check_if_provided_context_has_correct_structure(context)
end
@doc """
Add a Node to a clause and receive the updated context.
Provide the `context`, `alias` (as atom) of the node and `option` as a list of labels (as Strings) or a map of properties.
The function can be used along with `MATCH, OPTIONAL MATCH, CREATE, MERGE` clauses.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n, ["Person"]) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n:Person) RETURN n"
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n, %{age: 30, name: "John"}) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n {age: 30, name: 'John'}) RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.node(:n, ["Person"]) |> Query.return(:n) |> Query.build_query()
# error will hold
# "MATCH or OPTIONAL MATCH or CREATE or MERGE clause has to be provided first before using node(). E.g. new() |> match() |> node(:n) |> ..."
```
"""
@spec node(t(), atom(), list(String.t()) | map()) :: t()
def node(%{error: nil} = context, alias, option) when is_list(option) do
node(context, alias, option, %{})
end
def node(%{error: nil} = context, alias, option) when is_map(option) do
node(context, alias, [], option)
end
@doc """
Add a Node to a clause and receive the updated context.
Provide the `context`, `alias` (as atom) of the node you want to add, a list of `labels` (as Strings) and a map of `properties`.
The function can be used along with `MATCH, OPTIONAL MATCH, CREATE, MERGE` clauses.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n, ["Person"], %{age: 30, name: "John"}) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n:Person {age: 30, name: 'John'}) RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.node(:n, ["Person"], %{age: 30, name: "John"}) |> Query.return(:n) |> Query.build_query()
# error will hold
# "MATCH or OPTIONAL MATCH or CREATE or MERGE clause has to be provided first before using node(). E.g. new() |> match() |> node(:n) |> ..."
```
"""
@spec node(t(), atom(), list(String.t()) | [], map()) :: t()
def node(context, alias, labels \\ [], properties \\ %{})
def node(%{error: nil} = context, alias, labels, properties)
when is_list(labels) and is_map(properties) do
node = RedisGraph.Node.new(%{alias: alias, labels: labels, properties: properties})
last_element = Map.get(context, :last_element)
context = check_if_provided_context_has_correct_structure(context)
context = check_if_alias_is_atom(context, alias)
only_strings_in_labels_list? =
Stream.map(labels, fn label -> is_binary(label) end) |> Enum.all?()
context =
if(only_strings_in_labels_list?) do
context
else
Map.put(
context,
:error,
"Provided labels must all be of string type."
)
end
context = check_if_match_or_create_or_merge_clause_provided(context, "node()", false)
error = Map.get(context, :error)
case error do
nil ->
context =
if(is_struct(last_element, Relationship)) do
alias = Map.get(last_element, :alias)
dest_node = Map.get(last_element, :dest_node)
new_relationship =
if(is_nil(dest_node)) do
Map.put(last_element, :dest_node, node)
else
Map.put(last_element, :src_node, node)
end
{_old_value, updated_context} =
Map.get_and_update(context, :relationships, fn relationships ->
{relationships, Map.put(relationships, alias, new_relationship)}
end)
updated_context
else
context
end
{_old_value, context} =
Map.get_and_update(context, :nodes, fn old_map ->
{old_map, Map.put(old_map, alias, node)}
end)
context = update_used_clauses_with_data(context, alias)
context = Map.put(context, :last_element, node)
context
_ ->
context
end
end
def node(%{error: nil} = context, alias, _labels, _properties) do
context = check_if_provided_context_has_correct_structure(context)
%{error: error} = context
case error do
nil ->
Map.put(
context,
:error,
"Wrong parameters provided to node(:#{alias})"
)
_ ->
context
end
end
def node(context, _alias, _labels, _properties) do
check_if_provided_context_has_correct_structure(context)
end
@doc """
Add a Relationship to a clause and receive the updated context.
Provide the `context`, `alias` (as atom) of the relationship you want to add and an `option` as
of relation type (as Strings) or a map of properties.
The function can be used along with `MATCH, OPTIONAL MATCH, CREATE, MERGE` clauses.
relationship_from_to() will convert to `(:from_node)-[:rel]->(:to_node)`
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match |> Query.node(:n) |> Query.relationship_from_to(:r, "KNOWS") |> Query.node(:m) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n)-[r:KNOWS]->(m) RETURN n"
{:ok, query} = Query.new() |> Query.match |> Query.node(:n) |> Query.relationship_from_to(:r, %{duration: 100}) |> Query.node(:m) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n)-[r {duration: 100}]->(m) RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match |> Query.node(:n) |> Query.relationship_from_to(:r, %{duration: 100}) |> Query.return(:n) |> Query.build_query()
# error will hold
# "MATCH clause cannot end with a Relationship, add a Node at the end. E.g. new() |> match() |> node(:n) |> relationship_from_to(:r) |> node(:m) |> ..."
```
"""
@spec relationship_from_to(t(), atom(), String.t() | map()) :: t()
def relationship_from_to(%{error: nil} = context, alias, option) when is_binary(option) do
relationship_from_to(context, alias, option, %{})
end
def relationship_from_to(%{error: nil} = context, alias, option) when is_map(option) do
relationship_from_to(context, alias, "", option)
end
@doc """
Add a Relationship to a clause and receive the updated context.
Provide the `context`, `alias` (as atom) of the relationship you want to add, a `type` (as Strings) or a map of `properties`.
The function can be used along with `MATCH, OPTIONAL MATCH, CREATE, MERGE` clauses.
relationship_from_to() will convert to `(:from_node)-[:rel]->(:to_node)`
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match |> Query.node(:n) |> Query.relationship_from_to(:r, "TRAVELS", %{duration: 100}) |> Query.node(:m) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n)-[r:TRAVELS {duration: 100}]->(m) RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match |> Query.node(:n) |> Query.relationship_from_to(:r, "TRAVELS", %{duration: 100}) |> Query.return(:n) |> Query.build_query()
# error will hold
# "MATCH clause cannot end with a Relationship, add a Node at the end. E.g. new() |> match() |> node(:n) |> relationship_from_to(:r) |> node(:m) |> ..."
```
"""
@spec relationship_from_to(t(), atom(), String.t(), map()) :: t()
def relationship_from_to(context, alias, type \\ "", properties \\ %{})
def relationship_from_to(%{error: nil} = context, alias, type, properties)
when is_binary(type) and is_map(properties) do
last_element = Map.get(context, :last_element)
context = check_if_provided_context_has_correct_structure(context)
context =
if(is_nil(last_element)) do
Map.put(
context,
:error,
"Relationship has to originate from a Node. Add a Node first with node() function"
)
else
context
end
context =
if(is_struct(last_element, Relationship)) do
context =
Map.put(
context,
:error,
"You cannot have multiple Relationships in a row. Add a Node between them with node() function"
)
context = Map.put(context, :last_element, nil)
context
else
context
end
current_clause = Map.get(context, :current_clause)
context =
if(current_clause == :create and type == "") do
Map.put(
context,
:error,
"When you create a relationship, the type has to be provided. E.g. new() |> match() |> node(:n) |> relationship_from_to(:r, \"WORKS\") |> ..."
)
else
context
end
context = check_if_alias_is_atom(context, alias)
context =
check_if_match_or_create_or_merge_clause_provided(context, "relationship_from_to()", false)
error = Map.get(context, :error)
case error do
nil ->
relationship =
RedisGraph.Relationship.new(%{
alias: alias,
type: type,
properties: properties,
src_node: last_element
})
{_old_value, context} =
Map.get_and_update(context, :relationships, fn old_map ->
{old_map, Map.put(old_map, alias, relationship)}
end)
context = update_used_clauses_with_data(context, alias)
context = Map.put(context, :last_element, relationship)
context
_ ->
context
end
end
def relationship_from_to(%{error: nil} = context, alias, _type, _properties) do
context = check_if_provided_context_has_correct_structure(context)
%{error: error} = context
case error do
nil ->
Map.put(
context,
:error,
"Wrong parameters provided to relationship_from_to(:#{alias})"
)
_ ->
context
end
end
def relationship_from_to(context, _alias, _type, _properties) do
check_if_provided_context_has_correct_structure(context)
end
@doc """
Add a Relationship to a clause and receive the updated context.
Provide the `context`, `alias` (as atom) of the relationship you want to add and a `type` (as Strings) or a map of `properties`.
The function can be used along with `MATCH, OPTIONAL MATCH, CREATE, MERGE` clauses.
relationship_to_from() will convert to `(:to_node)<-[:rel]-(:from_node)`
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match |> Query.node(:n) |> Query.relationship_to_from(:r, "KNOWS") |> Query.node(:m) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n)<-[r:KNOWS]-(m) RETURN n"
{:ok, query} = Query.new() |> Query.match |> Query.node(:n) |> Query.relationship_to_from(:r, %{duration: 100}) |> Query.node(:m) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n)<-[r {duration: 100}]-(m) RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match |> Query.node(:n) |> Query.relationship_to_from(:r, %{duration: 100}) |> Query.return(:n) |> Query.build_query()
# error will hold
# "MATCH clause cannot end with a Relationship, add a Node at the end. E.g. new() |> match() |> node(:n) |> relationship_from_to(:r) |> node(:m) |> ..."
```
"""
@spec relationship_to_from(t(), atom(), String.t() | map()) :: t()
def relationship_to_from(%{error: nil} = context, alias, option) when is_binary(option) do
relationship_to_from(context, alias, option, %{})
end
def relationship_to_from(%{error: nil} = context, alias, option) when is_map(option) do
relationship_to_from(context, alias, "", option)
end
@doc """
Add a Relationship to a clause and receive the updated context.
Provide the `context`, `alias` (as atom) of the relationship you want to add and an `option` as
of relation type (as Strings) or a map of properties.
The function can be used along with `MATCH, OPTIONAL MATCH, CREATE, MERGE` clauses.
relationship_to_from() will convert to `(:to_node)<-[:rel]-(:from_node)`
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match |> Query.node(:n) |> Query.relationship_to_from(:r, "TRAVELS", %{duration: 100}) |> Query.node(:m) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n)<-[r:TRAVELS {duration: 100}]-(m) RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match |> Query.node(:n) |> Query.relationship_to_from(:r, "TRAVELS", %{duration: 100}) |> Query.return(:n) |> Query.build_query()
# error will hold
# "MATCH clause cannot end with a Relationship, add a Node at the end. E.g. new() |> match() |> node(:n) |> relationship_from_to(:r) |> node(:m) |> ..."
```
"""
@spec relationship_to_from(t(), atom(), String.t(), map()) :: t()
def relationship_to_from(context, alias, type \\ "", properties \\ %{})
def relationship_to_from(%{error: nil} = context, alias, type, properties)
when is_binary(type) and is_map(properties) do
last_element = Map.get(context, :last_element)
context = check_if_provided_context_has_correct_structure(context)
context =
if(is_nil(last_element)) do
Map.put(
context,
:error,
"Relationship has to point to a Node. Add a Node first with node() function"
)
else
context
end
context =
if(is_struct(last_element, Relationship)) do
context =
Map.put(
context,
:error,
"You cannot have multiple Relationships in a row. Add a Node between them with node() function"
)
context = Map.put(context, :last_element, nil)
context
else
context
end
current_clause = Map.get(context, :current_clause)
context =
if(current_clause == :create and type == "") do
Map.put(
context,
:error,
"When you create a relationship, the type has to be provided. E.g. new() |> match() |> node(:n) |> relationship_from_to(:r, \"WORKS\") |> ..."
)
else
context
end
context = check_if_alias_is_atom(context, alias)
context =
check_if_match_or_create_or_merge_clause_provided(context, "relationship_to_from()", false)
error = Map.get(context, :error)
case error do
nil ->
relationship =
RedisGraph.Relationship.new(%{
alias: alias,
type: type,
properties: properties,
dest_node: last_element
})
{_old_value, context} =
Map.get_and_update(context, :relationships, fn old_map ->
{old_map, Map.put(old_map, alias, relationship)}
end)
context = update_used_clauses_with_data(context, alias)
context = Map.put(context, :last_element, relationship)
context
_ ->
context
end
end
def relationship_to_from(%{error: nil} = context, alias, _type, _properties) do
context = check_if_provided_context_has_correct_structure(context)
%{error: error} = context
case error do
nil ->
Map.put(
context,
:error,
"Wrong parameters provided to relationship_to_from(:#{alias})"
)
_ ->
context
end
end
def relationship_to_from(context, _alias, _type, _properties) do
check_if_provided_context_has_correct_structure(context)
end
@doc """
Add `WHERE` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity you want to filter on,
a `property` for the given entity, a single `operator` (as atom) and a `value`.
Supported values(on the left) and operators (on the left):
- type `String` -> provide `:equals, :not_equal, :bigger, :bigger_or_equal, :smaller, :smaller_or_equal, :starts_with, :ends_with, :contains`
- type `number` -> `:equals, :not_equal, :bigger, :bigger_or_equal, :smaller, :smaller_or_equal`
- type `boolean` -> `:equals`
- type `nil` -> `:is, :is_not`
- type `list` -> `:in`
The operator will be converted to:
- :equals -> "="
- :not_equal -> "<>"
- :bigger -> ">"
- :bigger_or_equal -> ">="
- :smaller -> "<"
- :smaller_or_equal -> "<="
- :starts_with -> "STARTS WITH"
- :ends_with -> "ENDS WITH"
- :contains -> "CONTAINS"
- :in -> "IN"
- :is -> "IS"
- :is_not -> "IS NOT
where() can be used just once. If you want to have several conditions in `WHERE` clause, use
where() along with other functions, such as or_where()/or_not_where() etc.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where(:n, "age", :bigger, 5) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n) WHERE n.age > 5 RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where(:n, "age", :test, 5) |> Query.return(:n) |> Query.build_query()
# error will hold
# "Provided value: 5 or/and operator: :test in the WHERE clause is not supported."
```
"""
@spec where(
t(),
atom(),
String.t(),
accepted_operator_in_where_clause(),
accepted_value()
) :: t()
def where(context, alias, property, operator, value) do
where(context, alias, property, operator, value, :none)
end
defp where(%{error: nil} = context, _alias, "", _operator, _value, _logical_operator) do
context = check_if_provided_context_has_correct_structure(context)
%{error: error} = context
case error do
nil ->
Map.put(
context,
:error,
"Provide property name. E.g. new() |> match() |> node(:n) |> where(:n, \"age\", :bigger, 20}) |> return(:n) |> ..."
)
_ ->
context
end
end
defp where(%{error: nil} = context, _alias, _property, _operator, "", _logical_operator) do
context = check_if_provided_context_has_correct_structure(context)
%{error: error} = context
case error do
nil ->
Map.put(
context,
:error,
"Value can't be of empty string. E.g. new() |> match() |> node(:n) |> where({:n, \"age\", :contains, \"A\") |> return(:n) |> ..."
)
_ ->
context
end
end
defp where(%{error: nil} = context, alias, property, operator, value, logical_operator) do
# check if where clause with :none or :not exists and in that case not give error
# where or where_not put as first element and rest should be after it. where/where_not can be only once and after that only and/or func can be used
current_clause = Map.get(context, :current_clause)
context = check_if_provided_context_has_correct_structure(context)
context =
if current_clause != :where do
context = add_clause_if_not_present(context, :where)
Map.put(context, :current_clause, :where)
else
context
end
where_clause_elements_size =
Map.get(context, :used_clauses_with_data, [])
|> List.last(%{})
|> Map.get(:elements, [])
|> length
accepted_logical_operators =
if(where_clause_elements_size == 0,
do: [:none, :not],
else: [:and, :or, :xor, :and_not, :or_not, :xor_not]
)
logical_operator_accepted? = Enum.member?(accepted_logical_operators, logical_operator)
context =
if(logical_operator_accepted?) do
context
else
Map.put(
context,
:error,
"Provided order of WHERE clauses is wrong. You first call either where() or where_not() and then any number of the following or_where()/and_where()/or_not_where() etc. " <>
"E.g. new() |> match() |> node(:n) |> where(:n, \"age\", :bigger, 20) |> and_where(:n, \"name\", :contains, \"A\") |> return(:n) |> ..."
)
end
context = check_if_provided_alias_present(context, alias)
context = check_if_match_ends_with_relationship(context)
context = check_if_alias_is_atom(context, alias)
context = check_if_match_or_create_or_merge_clause_provided(context, :where)
context =
if(Map.has_key?(@accepted_operator_to_string_in_where_clause, operator)) do
context
else
Map.put(
context,
:error,
"Provided value: #{value} or/and operator: :#{operator} in the WHERE clause is not supported."
)
end
value_accepted? =
cond do
is_number(value) && Enum.member?(@accepted_operator_for_number_in_where_clause, operator) ->
true
is_binary(value) && Enum.member?(@accepted_operator_for_binary_in_where_clause, operator) ->
true
is_nil(value) && Enum.member?(@accepted_operator_for_nil_in_where_clause, operator) ->
true
is_boolean(value) &&
Enum.member?(@accepted_operator_for_boolean_in_where_clause, operator) ->
true
is_list(value) && Enum.member?(@accepted_operator_for_list_in_where_clause, operator) ->
true
true ->
false
end
context =
if(value_accepted?) do
context
else
Map.put(
context,
:error,
"Provided value: #{value} or/and operator: :#{operator} in the WHERE clause is not supported."
)
end
%{error: error} = context
case error do
nil ->
content = %{
alias: alias,
property: property,
operator: Map.get(@accepted_operator_to_string_in_where_clause, operator),
value: value
}
where_element = %{logical_operator: logical_operator, elements: [content]}
context = update_used_clauses_with_data(context, where_element)
context = Map.put(context, :current_clause, :where)
context
_ ->
context
end
end
defp where(context, _alias, _property, _operator, _value, _logical_operator) do
check_if_provided_context_has_correct_structure(context)
end
@doc """
Add `WHERE NOT` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity you want to filter on,
a `property` for the given entity, a single `operator` (as atom) and a `value`.
Check where() to see the supported values and operators.
where_not() can be used just once. If you want to have several conditions in `WHERE` clause, use
where_not() along with other functions, such as or_where()/or_not_where() etc.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where_not(:n, "age", :bigger, 5) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n) WHERE NOT n.age > 5 RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where_not(:n, "age", :test, 5) |> Query.return(:n) |> Query.build_query()
# error will hold
# "Provided value: 5 or/and operator: :test in the WHERE clause is not supported."
```
"""
@spec where_not(
t(),
atom(),
String.t(),
accepted_operator_in_where_clause(),
String.t() | number() | boolean() | list() | nil
) :: t()
def where_not(context, alias, property, operator, value) do
where(context, alias, property, operator, value, :not)
end
@doc """
Add `WHERE ... OR ...` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity you want to filter on,
a `property` for the given entity, a single `operator` (as atom) and a `value`.
Check where() to see the supported values and operators.
or_where() is used when you want to have several logical conditions is `WHERE` clause,
so where()/where_not() already needs to present as well.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where(:n, "age", :bigger, 5) |> Query.or_where(:n, "name", :contains, "A") |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n) WHERE n.age > 5 OR n.name CONTAINS 'A' RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where(:n, "age", :bigger, 5) |> Query.or_where(:n, "name", :test, "A") |> Query.return(:n) |> Query.build_query()
# error will hold
# "Provided value: 5 or/and operator: :test in the WHERE clause is not supported."
```
"""
@spec or_where(
t(),
atom(),
String.t(),
accepted_operator_in_where_clause(),
String.t() | number() | boolean() | list() | nil
) :: t()
def or_where(context, alias, property, operator, value) do
where(context, alias, property, operator, value, :or)
end
@doc """
Add `WHERE ... AND ...` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity you want to filter on,
a `property` for the given entity, a single `operator` (as atom) and a `value`.
Check where() to see the supported values and operators.
and_where() is used when you want to have several logical conditions is `WHERE` clause,
so where()/where_not() already needs to present as well.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where(:n, "age", :bigger, 5) |> Query.and_where(:n, "name", :contains, "A") |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n) WHERE n.age > 5 AND n.name CONTAINS 'A' RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where(:n, "age", :bigger, 5) |> Query.and_where(:n, "name", :test, "A") |> Query.return(:n) |> Query.build_query()
# error will hold
# "Provided value: 5 or/and operator: :test in the WHERE clause is not supported."
```
"""
@spec and_where(
t(),
atom(),
String.t(),
accepted_operator_in_where_clause(),
String.t() | number() | boolean() | list() | nil
) :: t()
def and_where(context, alias, property, operator, value) do
where(context, alias, property, operator, value, :and)
end
@doc """
Add `WHERE ... XOR ...` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity you want to filter on,
a `property` for the given entity, a single `operator` (as atom) and a `value`.
Check where() to see the supported values and operators.
xor_where() is used when you want to have several logical conditions is `WHERE` clause,
so where()/where_not() already needs to present as well.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where(:n, "age", :bigger, 5) |> Query.xor_where(:n, "name", :contains, "A") |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n) WHERE n.age > 5 XOR n.name CONTAINS 'A' RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where(:n, "age", :bigger, 5) |> Query.xor_where(:n, "name", :test, "A") |> Query.return(:n) |> Query.build_query()
# error will hold
# "Provided value: 5 or/and operator: :test in the WHERE clause is not supported."
```
"""
@spec xor_where(
t(),
atom(),
String.t(),
accepted_operator_in_where_clause(),
String.t() | number() | boolean() | list() | nil
) :: t()
def xor_where(context, alias, property, operator, value) do
where(context, alias, property, operator, value, :xor)
end
@doc """
Add `WHERE ... OR NOT ...` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity you want to filter on,
a `property` for the given entity, a single `operator` (as atom) and a `value`.
Check where() to see the supported values and operators.
or_not_where() is used when you want to have several logical conditions is `WHERE` clause,
so where()/where_not() already needs to present as well.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where(:n, "age", :bigger, 5) |> Query.or_not_where(:n, "name", :contains, "A") |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n) WHERE n.age > 5 OR NOT n.name CONTAINS 'A' RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where(:n, "age", :bigger, 5) |> Query.or_not_where(:n, "name", :test, "A") |> Query.return(:n) |> Query.build_query()
# error will hold
# "Provided value: 5 or/and operator: :test in the WHERE clause is not supported."
```
"""
@spec or_not_where(
t(),
atom(),
String.t(),
accepted_operator_in_where_clause(),
String.t() | number() | boolean() | list() | nil
) :: t()
def or_not_where(context, alias, property, operator, value) do
where(context, alias, property, operator, value, :or_not)
end
@doc """
Add `WHERE ... AND NOT ...` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity you want to filter on,
a `property` for the given entity, a single `operator` (as atom) and a `value`.
Check where() to see the supported values and operators.
and_not_where() is used when you want to have several logical conditions is `WHERE` clause,
so where()/where_not() already needs to present as well.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where(:n, "age", :bigger, 5) |> Query.and_not_where(:n, "name", :contains, "A") |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n) WHERE n.age > 5 AND NOT n.name CONTAINS 'A' RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where(:n, "age", :bigger, 5) |> Query.and_not_where(:n, "name", :test, "A") |> Query.return(:n) |> Query.build_query()
# error will hold
# "Provided value: 5 or/and operator: :test in the WHERE clause is not supported."
```
"""
@spec and_not_where(
t(),
atom(),
String.t(),
accepted_operator_in_where_clause(),
String.t() | number() | boolean() | list() | nil
) :: t()
def and_not_where(context, alias, property, operator, value) do
where(context, alias, property, operator, value, :and_not)
end
@doc """
Add `WHERE ... XOR NOT ...` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity you want to filter on,
a `property` for the given entity, a single `operator` (as atom) and a `value`.
Check where() to see the supported values and operators.
xor_not_where() is used when you want to have several logical conditions is `WHERE` clause,
so where()/where_not() already needs to present as well.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where(:n, "age", :bigger, 5) |> Query.xor_not_where(:n, "name", :contains, "A") |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n) WHERE n.age > 5 XOR NOT n.name CONTAINS 'A' RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.where(:n, "age", :bigger, 5) |> Query.xor_not_where(:n, "name", :test, "A") |> Query.return(:n) |> Query.build_query()
# error will hold
# "Provided value: 5 or/and operator: :test in the WHERE clause is not supported."
```
"""
@spec xor_not_where(
t(),
atom(),
String.t(),
accepted_operator_in_where_clause(),
String.t() | number() | boolean() | list() | nil
) :: t()
def xor_not_where(context, alias, property, operator, value) do
where(context, alias, property, operator, value, :xor_not)
end
@doc """
Add `ORDER BY` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity, a `property`
for the given entity on which you want to order and a `asc` boolean
(if set to `true`, the order is ASC and if `false`, order is DESC).
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.return(:n) |> Query.order_by(:n, "age") |> Query.build_query()
# query will hold
# "MATCH (n) RETURN n ORDER BY n.age ASC"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.return(:n) |> Query.order_by(:n, "") |> Query.build_query()
# error will hold
# "Provide property name. E.g. new() |> match() |> node(:n) |> order_by(:n, \"age\") |> return(:n) |> ..."
```
"""
@spec order_by(t(), atom(), String.t(), boolean()) :: t()
def order_by(context, alias, property, asc \\ true)
def order_by(%{error: nil} = context, _alias, "", _asc) do
context = check_if_provided_context_has_correct_structure(context)
%{error: error} = context
case error do
nil ->
Map.put(
context,
:error,
"Provide property name. E.g. new() |> match() |> node(:n) |> order_by(:n, \"age\") |> return(:n) |> ..."
)
_ ->
context
end
end
def order_by(%{error: nil} = context, alias, property, asc) when is_binary(property) do
current_clause = Map.get(context, :current_clause)
context = check_if_provided_context_has_correct_structure(context)
context =
if current_clause != :order_by do
context = add_clause_if_not_present(context, :order_by)
Map.put(context, :current_clause, :order_by)
else
context
end
context = check_if_provided_alias_present(context, alias)
context = check_if_match_ends_with_relationship(context)
context = check_if_alias_is_atom(context, alias)
context = check_if_match_or_create_or_merge_clause_provided(context, :order_by)
%{error: error} = context
case error do
nil ->
order = if(asc, do: "ASC", else: "DESC")
order_by_element = %{alias: alias, property: property, order: order}
context = update_used_clauses_with_data(context, order_by_element)
context = Map.put(context, :current_clause, :order_by)
context
_ ->
context
end
end
def order_by(%{error: nil} = context, _alias, _property, _asc) do
context = check_if_provided_context_has_correct_structure(context)
%{error: error} = context
case error do
nil ->
Map.put(
context,
:error,
"Wrong parameters provided. E.g. new() |> match() |> node(:n) |> order_by(:n, \"age\") |> return(:n) |> ..."
)
_ ->
context
end
end
def order_by(context, _alias, _property, _asc) do
check_if_provided_context_has_correct_structure(context)
end
@doc """
Add `LIMIT` clause into the context and receive the updated context.
Provide the `context` and `number` of rows you want to limit to.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.return(:n) |> Query.limit(10)|> Query.build_query()
# query will hold
# "MATCH (n) RETURN n LIMIT 10"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.return(:n) |> Query.limit("test")|> Query.build_query()
# error will hold
# "Wrong number parameter was probided, only non negatibe integers supported. E.g. new() |> match() |> node(:n) |> return(:n) |> limit(10)|> build_query()"
```
"""
@spec limit(t(), non_neg_integer()) :: t()
def limit(%{error: nil} = context, number) do
current_clause = Map.get(context, :current_clause)
context = check_if_provided_context_has_correct_structure(context)
context =
if current_clause != :limit do
context = add_clause_if_not_present(context, :limit)
Map.put(context, :current_clause, :limit)
else
context
end
context =
if(is_number(number) and number > 0) do
context
else
Map.put(
context,
:error,
"Wrong number parameter was probided, only non negatibe integers supported. E.g. new() |> match() |> node(:n) |> return(:n) |> limit(10)|> build_query()"
)
end
context = check_if_match_or_create_or_merge_clause_provided(context, :limit)
%{error: error} = context
case error do
nil ->
context = update_used_clauses_with_data(context, number)
context = Map.put(context, :current_clause, :limit)
context
_ ->
context
end
end
def limit(context, _number) do
check_if_provided_context_has_correct_structure(context)
end
@doc """
Add `SKIP` clause into the context and receive the updated context.
Provide the `context` and `number` of rows you want to skip.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.return(:n) |> Query.skip(10)|> Query.build_query()
# query will hold
# "MATCH (n) RETURN n SKIP 10"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.return(:n) |> Query.skip("test")|> Query.build_query()
# error will hold
# "Wrong number parameter was probided, only non negatibe integers supported. E.g. new() |> match() |> node(:n) |> return(:n) |> skip(10)|> build_query()"
```
"""
@spec skip(t(), non_neg_integer()) :: t()
def skip(%{error: nil} = context, number) do
current_clause = Map.get(context, :current_clause)
context = check_if_provided_context_has_correct_structure(context)
context =
if current_clause != :skip do
context = add_clause_if_not_present(context, :skip)
Map.put(context, :current_clause, :skip)
else
context
end
context =
if(is_number(number) and number > 0) do
context
else
Map.put(
context,
:error,
"Wrong number parameter was probided, only non negatibe integers supported. E.g. new() |> match() |> node(:n) |> return(:n) |> skip(10)|> build_query()"
)
end
context = check_if_match_ends_with_relationship(context)
context = check_if_match_or_create_or_merge_clause_provided(context, :skip)
%{error: error} = context
case error do
nil ->
context = update_used_clauses_with_data(context, number)
context = Map.put(context, :current_clause, :skip)
context
_ ->
context
end
end
def skip(context, _number) do
check_if_provided_context_has_correct_structure(context)
end
@doc """
Add `RETURN` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity you want to return
and an atom `as` that would hold the new name of the result.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n, ["Person"]) |> Query.return(:n, :Person) |> Query.build_query()
# query will hold
# "MATCH (n:Person) RETURN n AS Person"
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n, ["Person"]) |> Query.node(:m, ["Dog"]) |> Query.return(:n) |> Query.return(:m) |> Query.build_query()
# query will hold
# "MATCH (n:Person),(m:Dog) RETURN n, m"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n, ["Person"]) |> Query.return(:m) |> Query.build_query()
# error will hold
# "Provided alias: :m was not mentioned before. Pass the alias first: e.g. new() |> match() |> node(:n) |> order_by_property(:n, \"age\") |> ..."
```
"""
@spec return(t(), atom(), atom() | nil) :: t()
def return(context, alias, as \\ nil) do
return_function_and_property(context, nil, alias, nil, as, false)
end
@doc """
Add `RETURN` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity, name of the `property`
you want to return and an atom `as` that would hold the new name of the result.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n, ["Person"]) |> Query.return_property(:n,"age", :Person) |> Query.build_query()
# query will hold
# "MATCH (n:Person) RETURN n.age AS Person"
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n, ["Person"]) |> Query.return(:n) |> Query.return_property(:n, "age") |> Query.build_query()
# query will hold
# "MATCH (n:Person) RETURN n, n.age"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n, ["Person"]) |> Query.return_property(:m, "") |> Query.build_query()
# error will hold
# "Provide property name. E.g. new() |> match() |> node(:n) |> return_property(:n, \"age\") |> ..."
```
"""
@spec return_property(t(), atom(), String.t(), atom() | nil) :: t()
def return_property(context, alias, property, as \\ nil) do
return_function_and_property(context, nil, alias, property, as, false)
end
@doc """
Add `RETURN` clause into the context and receive the updated context.
Provide the `context`, name of the `function` which should be called,
`alias` (as atom) of the entity you want to return and and atom
`as` that would hold the new name of the result.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n, ["Person"]) |> Query.return_property(:n,"age", :Person) |> Query.build_query()
# query will hold
# "MATCH (n) RETURN n, labels(n) AS Labels"
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.return(:n) |> Query.return_function("labels", :n, :Labels) |> Query.build_query()
# query will hold
# "MATCH (n) RETURN n, labels(n) AS Labels"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.return(:n) |> Query.return_function("", :n, :Labels) |> Query.build_query()
# error will hold
# "Provide function name. E.g. new() |> match() |> node(:n) |> return_function(\"toUpper\", :n) |> ..."
```
"""
@spec return_function(t(), String.t(), atom(), atom() | nil) :: t()
def return_function(context, function, alias, as \\ nil) do
return_function_and_property(context, function, alias, nil, as, false)
end
@doc """
Add `RETURN` clause into the context and receive the updated context.
Provide the `context`, name of the `function` which should be called,
`alias` (as atom) of the entity, name of the `property` you want to return
and and atom `as` that would hold the new name of the result.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.return(:n) |> Query.return_function_and_property("toUpper", :n, "name", :Name) |> Query.build_query()
# query will hold
# "MATCH (n) RETURN n, toUpper(n.name) AS Name"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} =Query.new() |> Query.match() |> Query.node(:n) |> Query.return(:n) |> Query.return_function_and_property("toUpper", :n, "name", "Name") |> Query.build_query()
# error will hold
# ""Provided as attribute: Name needs to be an atom. E.g. Query.new() |> Query.match() |> Query.node(:n) |> Query.return(:n, :Node) |> Query.build_query()"
```
"""
@spec return_function_and_property(t(), String.t(), atom(), String.t(), atom() | nil) :: t()
def return_function_and_property(context, function, alias, property, as \\ nil) do
return_function_and_property(context, function, alias, property, as, false)
end
@doc """
Add `RETURN DISTINCT` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity you want to return
and an atom `as` that would hold the new name of the result.
Look at return() for examples
"""
@spec return_distinct(t(), atom(), atom() | nil) :: t()
def return_distinct(context, alias, as \\ nil) do
return_function_and_property(context, nil, alias, nil, as, true)
end
@doc """
Add `RETURN DISTINCT` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity, name of the `property`
you want to return and an atom `as` that would hold the new name of the result.
Look at return_property() for examples
"""
@spec return_distinct_property(t(), atom(), String.t(), atom() | nil) :: t()
def return_distinct_property(context, alias, property, as \\ nil) do
return_function_and_property(context, nil, alias, property, as, true)
end
@doc """
Add `RETURN DISTINCT` clause into the context and receive the updated context.
Provide the `context`, name of the `function` which should be called,
`alias` (as atom) of the entity you want to return and and atom
`as` that would hold the new name of the result.
Look at return_function() for examples
"""
@spec return_distinct_function(t(), String.t(), atom(), atom() | nil) :: t()
def return_distinct_function(context, function, alias, as \\ nil) do
return_function_and_property(context, function, alias, nil, as, true)
end
@doc """
Add `RETURN DISTINCT` clause into the context and receive the updated context.
Provide the `context`, name of the `function` which should be called,
`alias` (as atom) of the entity, name of the `property` you want to return
and and atom `as` that would hold the new name of the result.
Look at return_function_and_property() for examples
"""
@spec return_distinct_function_and_property(t(), String.t(), atom(), String.t(), atom() | nil) ::
t()
def return_distinct_function_and_property(context, function, alias, property, as \\ nil) do
return_function_and_property(context, function, alias, property, as, true)
end
@spec return_function_and_property(
t(),
String.t() | nil,
atom(),
String.t() | nil,
atom() | nil,
boolean()
) :: t()
defp return_function_and_property(
%{error: nil} = context,
"",
_alias,
_property,
_asc,
_distinct
) do
context = check_if_provided_context_has_correct_structure(context)
%{error: error} = context
case error do
nil ->
Map.put(
context,
:error,
"Provide function name. E.g. new() |> match() |> node(:n) |> return_function(\"toUpper\", :n) |> ..."
)
_ ->
context
end
end
defp return_function_and_property(
%{error: nil} = context,
_function,
_alias,
"",
_asc,
_distinct
) do
context = check_if_provided_context_has_correct_structure(context)
%{error: error} = context
case error do
nil ->
Map.put(
context,
:error,
"Provide property name. E.g. new() |> match() |> node(:n) |> return_property(:n, \"age\") |> ..."
)
_ ->
context
end
end
defp return_function_and_property(
%{error: nil} = context,
function,
alias,
property,
as,
distinct
) do
clause = if(distinct == false, do: :return, else: :return_distinct)
current_clause = Map.get(context, :current_clause)
context = check_if_provided_context_has_correct_structure(context)
context =
if current_clause != clause do
context = add_clause_if_not_present(context, clause)
Map.put(context, :current_clause, clause)
else
context
end
context =
if(is_nil(as) or is_atom(as)) do
context
else
Map.put(
context,
:error,
"Provided as attribute: #{as} needs to be an atom. E.g. Query.new() |> Query.match() |> Query.node(:n) |> Query.return(:n, :Node) |> Query.build_query()"
)
end
context = check_if_provided_alias_present(context, alias)
context = check_if_match_ends_with_relationship(context)
context = check_if_alias_is_atom(context, alias)
match_clause_present? = Map.get(context, :used_clauses, []) |> Enum.member?(:match)
optional_match_clause_present? =
Map.get(context, :used_clauses, []) |> Enum.member?(:optional_match)
create_clause_present? = Map.get(context, :used_clauses, []) |> Enum.member?(:create)
merge_clause_present? = Map.get(context, :used_clauses, []) |> Enum.member?(:merge)
context =
if(
not match_clause_present? and not create_clause_present? and not merge_clause_present? and
not optional_match_clause_present?
) do
Map.put(
context,
:error,
"One of these clauses MATCH, CREATE, MERGE etc. has to be provided first before using RETURN. E.g. new() |> match() |> node(:n) |> return(:n) |> ..."
)
else
context
end
%{error: error} = context
case error do
nil ->
return_element = %{alias: alias, property: property, function: function, as: as}
context = update_used_clauses_with_data(context, return_element)
context = Map.put(context, :current_clause, clause)
context
_ ->
context
end
end
defp return_function_and_property(context, _function, _alias, _property, _as, _distinct) do
check_if_provided_context_has_correct_structure(context)
end
@doc """
Add `WITH` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity you want to chain
and an atom `as` that would hold the new name of the result.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.with(:n, :Node) |> Query.return(:Node) |> Query.build_query()
# query will hold
# "MATCH (n) WITH n AS Node RETURN Node"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.with(:m, :Node) |> Query.return(:Node) |> Query.build_query()
# error will hold
# "Provided alias: :m was not mentioned before. Pass the alias first: e.g. new() |> match() |> node(:n) |> with(:n, :Node) |> |> return(:n) ..."
```
"""
@spec with(t(), atom(), atom() | nil) :: t()
def with(context, alias, as \\ nil) do
with_function_and_property(context, nil, alias, nil, as)
end
@doc """
Add `WITH` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity, name of the `property`
you want to return and an atom `as` that would hold the new name of the result.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.with_property(:n,"age", :Age) |> Query.return(:Age) |> Query.build_query()
# query will hold
# "MATCH (n) WITH n.age AS Age RETURN Age"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.with_property(:m,"age", :Age) |> Query.return(:Node) |> Query.build_query()
# error will hold
# "Provided alias: :m was not mentioned before. Pass the alias first: e.g. new() |> match() |> node(:n) |> with(:n, :Node) |> |> return(:n) ..."
```
"""
@spec with_property(t(), atom(), String.t(), atom() | nil) :: t()
def with_property(context, alias, property, as \\ nil) do
with_function_and_property(context, nil, alias, property, as)
end
@doc """
Add `WITH` clause into the context and receive the updated context.
Provide the `context`, name of the `function` which should be called,
`alias` (as atom) of the entity you want to return and and atom
`as` that would hold the new name of the result.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.with_function("labels", :n, :Labels) |> Query.return(:Labels) |> Query.build_query()
# query will hold
# "MATCH (n) WITH labels(n) AS Labels RETURN Labels"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.with_function("labels", :m, :Labels) |> Query.return(:Labels) |> Query.build_query()
# error will hold
# "Provided alias: :m was not mentioned before. Pass the alias first: e.g. new() |> match() |> node(:n) |> with(:n, :Node) |> |> return(:n) ..."
```
"""
@spec with_function(t(), String.t(), atom(), atom() | nil) :: t()
def with_function(context, function, alias, as \\ nil) do
with_function_and_property(context, function, alias, nil, as)
end
@doc """
Add `WITH` clause into the context and receive the updated context.
Provide the `context`, name of the `function` which should be called,
`alias` (as atom) of the entity, name of the `property` you want to return
and and atom `as` that would hold the new name of the result.
Instead of entity alias, :* atom can be provided.
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.with_function_and_property("toUpper", :n, "Name", :Name) |> Query.return(:Name) |> Query.build_query()
# query will hold
# "MATCH (n) WITH toUpper(n.Name) AS Name RETURN Name"
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.with(:*) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n) WITH * RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n) |> Query.with_function_and_property("toUpper", :m, "Name", :Name) |> Query.return(:Name) |> Query.build_query()
# error will hold
# "Provided alias: :m was not mentioned before. Pass the alias first: e.g. new() |> match() |> node(:n) |> with(:n, :Node) |> |> return(:n) ..."
```
"""
@spec with_function_and_property(
t(),
String.t() | nil,
atom(),
String.t() | nil,
atom() | nil
) :: t()
def with_function_and_property(context, function, alias, property, as \\ nil)
def with_function_and_property(%{error: nil} = context, "", _alias, _property, _as) do
context = check_if_provided_context_has_correct_structure(context)
%{error: error} = context
case error do
nil ->
Map.put(
context,
:error,
"Provide function name. E.g. new() |> match() |> node(:n) |> with_function_and_property(\"toUpper\", :n, \"name\", :Name) |> return(:Name) |>..."
)
_ ->
context
end
end
def with_function_and_property(%{error: nil} = context, _function, _alias, "", _as) do
context = check_if_provided_context_has_correct_structure(context)
%{error: error} = context
case error do
nil ->
Map.put(
context,
:error,
"Provide property name. E.g. new() |> match() |> node(:n) |> with_function_and_property(\"toUpper\", :n, \"name\", :Name) |> return(:Name) |> ..."
)
_ ->
context
end
end
def with_function_and_property(%{error: nil} = context, function, alias, property, as) do
current_clause = Map.get(context, :current_clause)
context = check_if_provided_context_has_correct_structure(context)
context =
if current_clause != :with do
context = add_clause_if_not_present(context, :with)
Map.put(context, :current_clause, :with)
else
context
end
# %{error: error} = context
provided_wildcard? = alias == :*
variable_present? = Map.get(context, :variables, []) |> Enum.member?(alias)
alias_present? =
Map.get(context, :relationships, %{}) |> Map.has_key?(alias) or
Map.get(context, :nodes, %{}) |> Map.has_key?(alias)
context =
if(not provided_wildcard? and not alias_present? and not variable_present?) do
Map.put(
context,
:error,
"Provided alias: :#{alias} was not mentioned before. Pass the alias first: e.g. new() |> match() |> node(:n) |> with(:n, :Node) |> |> return(:n) ..."
)
else
context
end
context =
if(is_nil(as) or is_atom(as)) do
context
else
Map.put(
context,
:error,
"Provided as attribute: #{as} needs to be an atom. E.g. new() |> match() |> node(:n) |> with(:n, :Node) |> |> return(:n) ..."
)
end
context = check_if_match_ends_with_relationship(context)
context = check_if_alias_is_atom(context, alias)
context = check_if_match_or_create_or_merge_clause_provided(context, :with)
%{error: error} = context
case error do
nil ->
with_element = %{alias: alias, property: property, function: function, as: as}
context = update_used_clauses_with_data(context, with_element)
context =
if is_nil(as) do
context
else
{_old_value, context} =
Map.get_and_update(context, :variables, fn old_list ->
{old_list, old_list ++ [as]}
end)
context
end
context = Map.put(context, :current_clause, :with)
context
_ ->
context
end
end
def with_function_and_property(context, _function, _alias, _property, _as) do
context
end
@doc """
Add `SET` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity, new `value`
you want to set and `operator`.
`value` arbument can be of the following type:
- String
- number
- boolean
- nil
- list
- map
`operator` can be:
- "=" (default) -- for assignment
- "+=" -- for update
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n, %{age: 5, name: "John"}) |> Query.set(:n, %{}) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n {age: 5, name: 'John'}) SET n = {} RETURN n"
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n, %{age: 5, name: "John"}) |> Query.set(:n, %{works: false}, "+=") |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n {age: 5, name: 'John'}) SET n += {works: false} RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = uery.new() |> Query.match() |> Query.node(:n, %{age: 5, name: "John"}) |> Query.set(:m, %{}) |> Query.return(:n) |> Query.build_query()
# error will hold
# "Provided alias: :m was not mentioned before. Pass the alias first: e.g. new() |> match() |> node(:n) |> order_by_property(:n, \"age\") |> ..."
```
"""
@spec set(t(), atom(), accepted_value(), String.t()) :: t()
def set(context, alias, value, operator \\ "=")
def set(context, alias, value, operator) do
set_property_on(context, alias, nil, value, operator, :none)
end
@doc """
Add `SET` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity, name of the
`property`, new `value` you want to set and `operator`.
`value` arbument can be of the following type:
- String
- number
- boolean
- nil
- list
- map
`operator` can be:
- "=" (default) -- for assignment
- "+=" -- for update
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n, %{age: 5, name: "John"}) |> Query.set_property(:n, "age", 25) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n {age: 5, name: 'John'}) SET n.age = 25 RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = uery.new() |> Query.match() |> Query.node(:n, %{age: 5, name: "John"}) |> Query.set_property(:m, "age", 25) |> Query.return(:n) |> Query.build_query()
# error will hold
# "Provided alias: :m was not mentioned before. Pass the alias first: e.g. new() |> match() |> node(:n) |> order_by_property(:n, \"age\") |> ..."
```
"""
@spec set_property(t(), atom(), String.t(), accepted_value(), String.t()) :: t()
def set_property(context, alias, property, value, operator \\ "=")
def set_property(context, alias, property, value, operator) do
set_property_on(context, alias, property, value, operator, :none)
end
@doc """
Add `ON MATCH SET` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity, new `value`
you want to set and `operator`.
Should only be used when `MERGE` clause is provided first.
`value` arbument can be of the following type:
- String
- number
- boolean
- nil
- list
- map
`operator` can be:
- "=" (default) -- for assignment
- "+=" -- for update
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.merge() |> Query.node(:n, ["Person"], %{age: 5}) |> Query.on_match_set(:n, %{name: "Michael"}, "+=") |> Query.return(:n) |> Query.build_query()
# query will hold
# "MERGE (n:Person {age: 5}) ON MATCH SET n += {name: 'Michael'} RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = uery.new() |> Query.match() |> Query.node(:n, %{age: 5, name: "John"}) |> Query.on_match_set(:m, %{}) |> Query.return(:n) |> Query.build_query()
# error will hold
# "MERGE clause has to be provided first before using ON MATCH SET. E.g. new() |> merge() |> node(:n) |> node(:m) |> on_create_set(:n, \"m\") |> return(:n) |> ..."
```
"""
@spec on_match_set(t(), atom(), accepted_value(), String.t()) :: t()
def on_match_set(context, alias, value, operator \\ "=")
def on_match_set(context, alias, value, operator) do
set_property_on(context, alias, nil, value, operator, :match)
end
@doc """
Add `ON MATCH SET` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity, name of the
`property`, new `value` you want to set and `operator`.
Should only be used when `MERGE` clause is provided first.
`value` arbument can be of the following type:
- String
- number
- boolean
- nil
- list
- map
`operator` can be:
- "=" (default) -- for assignment
- "+=" -- for update
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.merge() |> Query.node(:n, ["Person"], %{age: 5}) |> Query.on_match_set_property(:n, "age", 50) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MERGE (n:Person {age: 5}) ON MATCH SET n.age = 50 RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n, ["Person"], %{age: 5}) |> Query.on_match_set_property(:n, "age", 50) |> Query.return(:n) |> Query.build_query()
# error will hold
# "MERGE clause has to be provided first before using ON MATCH SET. E.g. new() |> merge() |> node(:n) |> node(:m) |> on_create_set(:n, \"m\") |> return(:n) |> ..."
```
"""
@spec on_match_set_property(t(), atom(), String.t(), accepted_value(), String.t()) :: t()
def on_match_set_property(context, alias, property, value, operator \\ "=")
def on_match_set_property(context, alias, property, value, operator) do
set_property_on(context, alias, property, value, operator, :match)
end
@doc """
Add `ON CREATE SET` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity, new `value`
you want to set and `operator`.
Should only be used when `MERGE` clause is provided first.
`value` arbument can be of the following type:
- String
- number
- boolean
- nil
- list
- map
`operator` can be:
- "=" (default) -- for assignment
- "+=" -- for update
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.merge() |> Query.node(:n, ["Person"], %{age: 5}) |> Query.on_create_set(:n, %{name: "Michael"}, "+=") |> Query.return(:n) |> Query.build_query()
# query will hold
# "MERGE (n:Person {age: 5}) ON CREATE SET n += {name: 'Michael'} RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = uery.new() |> Query.match() |> Query.node(:n, %{age: 5, name: "John"}) |> Query.on_create_set(:m, %{}) |> Query.return(:n) |> Query.build_query()
# error will hold
# "MERGE clause has to be provided first before using ON CREATE SET. E.g. new() |> merge() |> node(:n) |> node(:m) |> on_create_set(:n, \"m\") |> return(:n) |> ..."
```
"""
@spec on_create_set(t(), atom(), accepted_value(), String.t()) :: t()
def on_create_set(context, alias, value, operator \\ "=")
def on_create_set(context, alias, value, operator) do
set_property_on(context, alias, nil, value, operator, :create)
end
@doc """
Add `ON CREATE SET` clause into the context and receive the updated context.
Provide the `context`, `alias` (as atom) of the entity, name of the
`property`, new `value` you want to set and `operator`.
Should only be used when `MERGE` clause is provided first.
`value` arbument can be of the following type:
- String
- number
- boolean
- nil
- list
- map
`operator` can be:
- "=" (default) -- for assignment
- "+=" -- for update
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.merge() |> Query.node(:n, ["Person"], %{age: 5}) |> Query.on_create_set_property(:n, "age", 50) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MERGE (n:Person {age: 5}) ON CREATE SET n.age = 50 RETURN n"
```
If the client uses the function incorrectly, the error will be persisted and
returned when the client will try to build the query.
## Example
```
{:error, query} = Query.new() |> Query.match() |> Query.node(:n, ["Person"], %{age: 5}) |> Query.on_create_set_property(:n, "age", 50) |> Query.return(:n) |> Query.build_query()
# error will hold
# "MERGE clause has to be provided first before using ON CREATE SET. E.g. new() |> merge() |> node(:n) |> node(:m) |> on_create_set(:n, \"m\") |> return(:n) |> ..."
```
"""
@spec on_create_set_property(t(), atom(), String.t(), accepted_value(), String.t()) :: t()
def on_create_set_property(context, alias, property, value, operator \\ "=")
def on_create_set_property(context, alias, property, value, operator) do
set_property_on(context, alias, property, value, operator, :create)
end
@spec set_property_on(
t(),
atom(),
String.t() | nil,
accepted_value(),
String.t(),
:none | :match | :create
) :: t()
defp set_property_on(context, alias, property, value, operator, on)
defp set_property_on(%{error: nil} = context, _alias, "", _value, _operator, _on) do
context = check_if_provided_context_has_correct_structure(context)
%{error: error} = context
case error do
nil ->
Map.put(
context,
:error,
"Provide property name. E.g. new() |> match() |> node(:n) |> set_property(:n, \"name\", :Name) |> return(:n) |> ..."
)
_ ->
context
end
end
defp set_property_on(%{error: nil} = context, alias, property, value, operator, on) do
clause =
case on do
:none -> :set
:match -> :on_match_set
:create -> :on_create_set
end
current_clause = Map.get(context, :current_clause)
context = check_if_provided_context_has_correct_structure(context)
context =
if current_clause != clause do
context = add_clause_if_not_present(context, clause)
Map.put(context, :current_clause, clause)
else
context
end
context =
if(operator == "=" or operator == "+=") do
context
else
Map.put(
context,
:error,
"Provided operator \"#{operator}\" is not supported. Only := (default) or :+= is supported. E.g. new() |> match() |> node(:n) |> node(:n) |> set_property(:n, \"age\", 100, :+=) |> ..."
)
end
context = check_if_provided_alias_present(context, alias)
# check if value is an atom which indicates that it is an alias, so an entity has to be set to another entity. E.g. new |> match |> node(:n) |> node(:m) |> set(:n, :m) |> ...
context =
if(is_atom(value) and is_nil(property),
do: check_if_provided_alias_present(context, value),
else: context
)
context = check_if_match_ends_with_relationship(context)
context = check_if_alias_is_atom(context, alias)
context =
if(clause == :set) do
clause_to_string = Atom.to_string(clause) |> String.replace("_", " ") |> String.upcase()
match_clause_present? = Map.get(context, :used_clauses, []) |> Enum.member?(:match)
optional_match_clause_present? =
Map.get(context, :used_clauses, []) |> Enum.member?(:optional_match)
create_clause_present? = Map.get(context, :used_clauses, []) |> Enum.member?(:create)
context =
if(
not match_clause_present? and not optional_match_clause_present? and
not create_clause_present?
) do
Map.put(
context,
:error,
"MATCH or OPTIONAL MATCH or CREATE clause has to be provided first before using #{clause_to_string}. E.g. new() |> match() |> node(:n) |> ..."
)
else
context
end
context
else
merge_clause_present? = Map.get(context, :used_clauses, []) |> Enum.member?(:merge)
clause_to_string = Atom.to_string(clause) |> String.replace("_", " ") |> String.upcase()
context =
if merge_clause_present? do
context
else
Map.put(
context,
:error,
"MERGE clause has to be provided first before using #{clause_to_string}. E.g. new() |> merge() |> node(:n) |> node(:m) |> on_create_set(:n, \"m\") |> return(:n) |> ..."
)
end
context
end
%{error: error} = context
case error do
nil ->
set_element = %{alias: alias, property: property, value: value, operator: operator}
context = update_used_clauses_with_data(context, set_element)
context = Map.put(context, :current_clause, clause)
context
_ ->
context
end
end
defp set_property_on(context, _alias, _property, _value, _operator, _on) do
check_if_provided_context_has_correct_structure(context)
end
@doc """
Function used to build the query string from the Redisgraph.Query context.
Function receives the `context` as argument and returns either `{:ok, query_string}`
or `{:error, error_message}`
## Example
```
alias RedisGraph.{Query}
{:ok, query} = Query.new() |> Query.match() |> Query.node(:n, ["Person"], %{age: 5}) |> Query.return(:n) |> Query.build_query()
# query will hold
# "MATCH (n:Person {age: 5}) RETURN n"
```
If the client uses the function incorrectly, the error will be returned.
## Example
```
{:error, query} = Query.match(%{}) |> Query.node(:n, ["Person"], %{age: 5}) |> Query.return(:n) |> Query.build_query()
# error will hold
# "Please instantiate the query first with new(). Istead have e.g. new() |> match |> node(:n) |> return(:n) |> build_query()"
```
"""
# @spec build_query(t()) :: {:ok, String.t()} | {:error, String.t()}
# def build_query(context) do
# context = check_if_match_ends_with_relationship(context)
# context = check_if_return_clause_is_provided_in_case_match_clause_is_present(context)
# context = check_if_provided_context_has_correct_structure(context)
# QueryBuilder.build_query(context)
# end
@spec build_query(t()) :: {:ok, String.t()} | {:error, String.t()}
def build_query(%{error: nil} = context) do
context = check_if_provided_context_has_correct_structure(context)
context = check_if_match_ends_with_relationship(context)
context = check_if_return_clause_is_provided_in_case_match_clause_is_present(context)
%{error: error} = context
case error do
nil ->
%{used_clauses_with_data: used_clauses_with_data} = context
query_list =
Stream.map(used_clauses_with_data, fn used_clause_with_data ->
%{clause: clause, elements: elements} = used_clause_with_data
case clause do
:create -> build_query_for_general_clause(context, clause, elements)
:match -> build_query_for_general_clause(context, clause, elements)
:optional_match -> build_query_for_general_clause(context, clause, elements)
:merge -> build_query_for_general_clause(context, clause, elements)
:delete -> build_query_for_delete_clause(context, elements)
:set -> build_query_for_set_clause(context, clause, elements)
:on_match_set -> build_query_for_set_clause(context, clause, elements)
:on_create_set -> build_query_for_set_clause(context, clause, elements)
:where -> build_query_for_where_clause(context, elements)
:order_by -> build_query_for_order_by_clause(context, elements)
:limit -> build_query_for_limit_or_skip_clause(context, clause, elements)
:skip -> build_query_for_limit_or_skip_clause(context, clause, elements)
:with -> build_query_for_return_or_with_clause(context, clause, elements)
:return -> build_query_for_return_or_with_clause(context, clause, elements)
:return_distinct -> build_query_for_return_or_with_clause(context, clause, elements)
_ -> "!!!Provided clause -- #{clause} is not yet supported!!!"
end
end)
final_query = Enum.join(query_list, " ")
{:ok, final_query}
_ ->
{:error, error}
end
end
def build_query(context) do
context = check_if_provided_context_has_correct_structure(context)
%{error: error} = context
{:error, error}
end
@spec build_query_for_general_clause(t(), atom(), list(map())) :: String.t()
defp build_query_for_general_clause(context, clause, elements) do
{_last_element, query} =
Enum.reduce(elements, {nil, ""}, fn element_alias, acc ->
{last_element, query} = acc
node = Map.get(context, :nodes, %{}) |> Map.get(element_alias, nil)
relationship = Map.get(context, :relationships, %{}) |> Map.get(element_alias, nil)
cond do
is_struct(node, Node) and is_struct(last_element, Node) ->
last_element = node
query =
query <>
",(#{Util.value_to_string(node.alias)}#{Util.labels_to_string(node.labels)}#{Util.properties_to_string(node.properties)})"
{last_element, query}
is_struct(node, Node) ->
last_element = node
query =
query <>
"(#{Util.value_to_string(node.alias)}#{Util.labels_to_string(node.labels)}#{Util.properties_to_string(node.properties)})"
{last_element, query}
is_struct(relationship, Relationship) and is_struct(last_element, Node) and
relationship.src_node.alias == last_element.alias ->
last_element = relationship
query =
query <>
"-[#{Util.value_to_string(relationship.alias)}#{Util.type_to_string(relationship.type)}#{Util.properties_to_string(relationship.properties)}]->"
{last_element, query}
is_struct(relationship, Relationship) and is_struct(last_element, Node) and
relationship.dest_node.alias == last_element.alias ->
last_element = relationship
query =
query <>
"<-[#{Util.value_to_string(relationship.alias)}#{Util.type_to_string(relationship.type)}#{Util.properties_to_string(relationship.properties)}]-"
{last_element, query}
true ->
last_element = nil
query = query <> "!!!something went wrong, check the query!!!"
{last_element, query}
end
end)
clause_to_string = Atom.to_string(clause) |> String.replace("_", " ") |> String.upcase()
"#{clause_to_string} #{query}"
end
@spec build_query_for_where_clause(t(), list(map())) :: String.t()
defp build_query_for_where_clause(_context, elements) do
query_list =
Stream.map(elements, fn element ->
%{logical_operator: logical_operator, elements: elements_per_logical_operator} = element
logical_operator_to_string =
if(logical_operator == :none,
do: "",
else:
(Atom.to_string(logical_operator) |> String.replace("_", " ") |> String.upcase()) <>
" "
)
inner_query_list =
Stream.map(elements_per_logical_operator, fn element_per_logical_operator ->
%{alias: alias, property: property, operator: operator, value: value} =
element_per_logical_operator
"#{logical_operator_to_string}#{Util.value_to_string(alias)}.#{property} #{operator} #{Util.value_to_string(value)}"
end)
Enum.join(inner_query_list, " ")
end)
query_list_joined = Enum.join(query_list, " ")
"WHERE #{query_list_joined}"
end
@spec build_query_for_return_or_with_clause(t(), atom(), list(map())) :: String.t()
defp build_query_for_return_or_with_clause(_context, clause, elements) do
clause_to_string = Atom.to_string(clause) |> String.replace("_", " ") |> String.upcase()
query_list =
Stream.map(elements, fn element ->
%{alias: alias, property: property, function: function, as: as} = element
cond do
not is_nil(alias) and not is_nil(property) and not is_nil(function) and not is_nil(as) ->
"#{function}(#{Util.value_to_string(alias)}.#{property}) AS #{as}"
not is_nil(alias) and not is_nil(property) and not is_nil(function) ->
"#{function}(#{Util.value_to_string(alias)}.#{property})"
not is_nil(alias) and not is_nil(function) and not is_nil(as) ->
"#{function}(#{Util.value_to_string(alias)}) AS #{as}"
not is_nil(alias) and not is_nil(function) ->
"#{function}(#{Util.value_to_string(alias)})"
not is_nil(alias) and not is_nil(property) and not is_nil(as) ->
"#{Util.value_to_string(alias)}.#{property} AS #{as}"
not is_nil(alias) and not is_nil(property) ->
"#{Util.value_to_string(alias)}.#{property}"
not is_nil(alias) and not is_nil(as) ->
"#{Util.value_to_string(alias)} AS #{as}"
not is_nil(alias) ->
"#{Util.value_to_string(alias)}"
true ->
"Wrong parameters provided to #{clause} function"
end
end)
query_list_joined = Enum.join(query_list, ", ")
"#{clause_to_string} #{query_list_joined}"
end
@spec build_query_for_order_by_clause(t(), list(map())) :: String.t()
defp build_query_for_order_by_clause(_context, elements) do
query_list =
Stream.map(elements, fn element ->
%{property: property, alias: alias, order: order} = element
"#{Util.value_to_string(alias)}.#{property} #{order}"
end)
query_list_joined = Enum.join(query_list, ", ")
"ORDER BY #{query_list_joined}"
end
@spec build_query_for_set_clause(t(), atom(), list(map())) :: String.t()
defp build_query_for_set_clause(_context, clause, elements) do
clause_to_string =
if(clause == :set,
do: "SET",
else: Atom.to_string(clause) |> String.replace("_", " ") |> String.upcase()
)
query_list =
Stream.map(elements, fn element ->
%{alias: alias, property: property, operator: operator, value: value} = element
"#{Util.value_to_string(alias)} #{operator} #{Util.value_to_string(value)}"
cond do
not is_nil(alias) and not is_nil(property) ->
"#{Util.value_to_string(alias)}.#{property} #{operator} #{Util.value_to_string(value)}"
not is_nil(alias) ->
"#{Util.value_to_string(alias)} #{operator} #{Util.value_to_string(value)}"
true ->
"Wrong parameters provided to #{clause} function"
end
end)
query_list_joined = Enum.join(query_list, ", ")
"#{clause_to_string} #{query_list_joined}"
end
@spec build_query_for_delete_clause(t(), list(map())) :: String.t()
defp build_query_for_delete_clause(_context, elements) do
query_list = Enum.join(elements, ", ")
"DELETE #{query_list}"
end
@spec build_query_for_limit_or_skip_clause(t(), atom(), list(map())) :: String.t()
defp build_query_for_limit_or_skip_clause(_context, clause, elements) do
clause_to_string = Atom.to_string(clause) |> String.upcase()
query_list = Enum.map(elements, fn element -> "#{clause_to_string} #{element}" end)
Enum.join(query_list, " ")
end
@spec add_clause_if_not_present(t(), atom()) :: t()
defp add_clause_if_not_present(%{error: nil} = context, clause) do
context = check_if_provided_context_has_correct_structure(context)
%{error: error} = context
case error do
nil ->
{_old_value, context} =
Map.get_and_update(context, :used_clauses, fn old_list ->
{old_list, old_list ++ [clause]}
end)
{_old_value, context} =
Map.get_and_update(context, :used_clauses_with_data, fn old_list ->
{old_list, old_list ++ [%{clause: clause, elements: []}]}
end)
context
_ ->
context
end
end
defp add_clause_if_not_present(context, _clause) do
context
end
@spec update_used_clauses_with_data(t(), map() | atom()) :: t()
defp update_used_clauses_with_data(%{error: nil} = context, data) do
# IO.puts("context")
# IO.inspect(context)
# IO.puts("data")
# IO.inspect(data)
used_clauses_with_data = Map.get(context, :used_clauses_with_data, [])
{_old_value, updated_last_clause} =
List.last(used_clauses_with_data, %{})
|> Map.get_and_update(:elements, fn old_elements ->
{old_elements, old_elements ++ [data]}
end)
updated_used_clauses_with_data =
List.replace_at(used_clauses_with_data, -1, updated_last_clause)
{_old_value, context} =
Map.get_and_update(context, :used_clauses_with_data, fn old_map ->
{old_map, updated_used_clauses_with_data}
end)
context
end
defp update_used_clauses_with_data(context, _data) do
context
end
@spec check_if_provided_alias_present(t(), atom()) :: t()
defp check_if_provided_alias_present(%{error: nil} = context, alias) do
alias_present? =
Map.get(context, :relationships, %{}) |> Map.has_key?(alias) or
Map.get(context, :nodes, %{}) |> Map.has_key?(alias)
variable_present? = Map.get(context, :variables, []) |> Enum.member?(alias)
if(not alias_present? and not variable_present?) do
Map.put(
context,
:error,
"Provided alias: :#{alias} was not mentioned before. Pass the alias first: e.g. new() |> match() |> node(:n) |> order_by_property(:n, \"age\") |> ..."
)
else
context
end
end
defp check_if_provided_alias_present(context, _alias) do
context
end
@spec check_if_alias_is_atom(t(), any()) :: t()
defp check_if_alias_is_atom(%{error: nil} = context, alias) do
if(is_atom(alias)) do
context
else
Map.put(
context,
:error,
"Provided alias is not an atom, only atoms are accepted. E.g. new() |> match() |> node(:n) |> relationship_from_to(:r) |> node(:m) |> ..."
)
end
end
defp check_if_alias_is_atom(context, _alias) do
context
end
@spec check_if_match_ends_with_relationship(t()) :: t()
defp check_if_match_ends_with_relationship(%{error: nil} = context) do
if(is_struct(Map.get(context, :last_element), Relationship)) do
Map.put(
context,
:error,
"MATCH clause cannot end with a Relationship, add a Node at the end. E.g. new() |> match() |> node(:n) |> relationship_from_to(:r) |> node(:m) |> ..."
)
else
context
end
end
defp check_if_match_ends_with_relationship(context) do
context
end
@spec check_if_match_or_create_or_merge_clause_provided(t(), atom()) :: t()
defp check_if_match_or_create_or_merge_clause_provided(context, clause, alter \\ true)
defp check_if_match_or_create_or_merge_clause_provided(%{error: nil} = context, clause, alter) do
clause_to_string =
if(alter,
do: Atom.to_string(clause) |> String.replace("_", " ") |> String.upcase(),
else: clause
)
match_clause_present? = Map.get(context, :used_clauses, []) |> Enum.member?(:match)
optional_match_clause_present? =
Map.get(context, :used_clauses, []) |> Enum.member?(:optional_match)
merge_clause_present? = Map.get(context, :used_clauses, []) |> Enum.member?(:merge)
create_clause_present? = Map.get(context, :used_clauses, []) |> Enum.member?(:create)
context =
if not match_clause_present? and not optional_match_clause_present? and
not create_clause_present? and not merge_clause_present? do
Map.put(
context,
:error,
"MATCH or OPTIONAL MATCH or CREATE or MERGE clause has to be provided first before using #{clause_to_string}. E.g. new() |> match() |> node(:n) |> ..."
)
else
context
end
context
end
defp check_if_match_or_create_or_merge_clause_provided(context, _clause, _alter) do
context
end
defp check_if_return_clause_already_provided(%{error: nil} = context, clause) do
clause_to_string = Atom.to_string(clause) |> String.replace("_", " ") |> String.upcase()
return_clause_present? = Map.get(context, :used_clauses, []) |> Enum.member?(:return)
return_distinct_clause_present? =
Map.get(context, :used_clauses, []) |> Enum.member?(:return_distinct)
context =
if return_clause_present? and return_distinct_clause_present? do
Map.put(
context,
:error,
"#{clause_to_string} can't be provided after RETURN or/and RETURN DISTINCT clause. Istead have e.g. new() |> match |> node(:n) |> node(:m) |> return(:n) |> return(:m)"
)
else
context
end
context
end
defp check_if_return_clause_already_provided(context, _clause) do
context
end
defp check_if_return_clause_is_provided_in_case_match_clause_is_present(%{error: nil} = context) do
match_clause_present? = Map.get(context, :used_clauses, []) |> Enum.member?(:match)
optional_match_clause_present? =
Map.get(context, :used_clauses, []) |> Enum.member?(:optional_match)
context =
if match_clause_present? or optional_match_clause_present? do
filtered_size =
Map.get(context, :used_clauses_with_data, [])
|> Enum.filter(fn %{clause: clause, elements: elements} ->
(clause == :match or clause == :optional_match) and length(elements) > 0
end)
|> length()
filtered_size_enough? = filtered_size > 0
return_clause_present? = Map.get(context, :used_clauses, []) |> Enum.member?(:return)
return_distinct_clause_present? =
Map.get(context, :used_clauses, []) |> Enum.member?(:return_distinct)
delete_clause_present? = Map.get(context, :used_clauses, []) |> Enum.member?(:delete)
set_clause_present? = Map.get(context, :used_clauses, []) |> Enum.member?(:set)
inner_context =
if (return_clause_present? or return_distinct_clause_present? or delete_clause_present? or
set_clause_present?) and filtered_size_enough? do
context
else
Map.put(
context,
:error,
"In case you provide MATCH, OPTIONAL MATCH - then RETURN, RETURN DISCTINCT, SET or DELETE also has to be provided. E.g. new() |> match |> node(:n) |> return(:n)"
)
end
inner_context
else
context
end
context
end
defp check_if_return_clause_is_provided_in_case_match_clause_is_present(context) do
context
end
defp check_if_provided_context_has_correct_structure(context) do
if is_struct(context, __MODULE__) do
context
else
new()
|> Map.put(
:error,
"Please instantiate the query first with new(). Istead have e.g. new() |> match |> node(:n) |> return(:n) |> build_query()"
)
end
end
end