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