defmodule Ravix.RQL.Query do @moduledoc """ Detructurized Raven Query Language structure """ defstruct from_token: nil, where_token: nil, update_token: nil, and_tokens: [], or_tokens: [], group_token: nil, select_token: nil, order_token: nil, limit_token: nil, query_string: "", query_params: %{}, params_count: 0, aliases: %{}, is_raw: false require OK alias Ravix.RQL.Query alias Ravix.RQL.QueryParser alias Ravix.RQL.Tokens.{ Where, Select, From, And, Or, Condition, Update, Limit, Not, Order, Group } alias Ravix.Documents.Session @type t :: %Query{ from_token: From.t() | nil, where_token: Where.t() | nil, update_token: Update.t() | nil, and_tokens: list(And.t()), or_tokens: list(Or.t()), group_token: Group.t() | nil, select_token: Select.t() | nil, order_token: Order.t() | nil, limit_token: Limit.t() | nil, query_string: String.t(), query_params: map(), params_count: non_neg_integer(), aliases: map(), is_raw: boolean() } @doc """ Creates a new query for the informed collection or index Returns a `Ravix.RQL.Query` or an `{:error, :query_document_must_be_informed}` if no collection/index was informed ## Examples iex> Ravix.RQL.Query.from("test") """ @spec from(nil | String.t()) :: {:error, :query_document_must_be_informed} | Query.t() def from(nil), do: {:error, :query_document_must_be_informed} def from(document) do %Query{ from_token: From.from(document) } end @doc """ Creates a new query with an alias for the informed collection or index Returns a `Ravix.RQL.Query` or an `{:error, :query_document_must_be_informed}` if no collection/index was informed ## Examples iex> Ravix.RQL.Query.from("test", "t") """ @spec from(nil | String.t(), String.t()) :: {:error, :query_document_must_be_informed} | Query.t() def from(nil, _), do: {:error, :query_document_must_be_informed} def from(document, as_alias) when not is_nil(as_alias) do %Query{ from_token: From.from(document), aliases: Map.put(%{}, document, as_alias) } end @doc """ Adds an update operation to the informed query, it supports a `Ravix.RQL.Tokens.Update` token. The token can be created using the following functions: `Ravix.RQL.Tokens.Update.set(%Update{}, field, new_value)` to set values `Ravix.RQL.Tokens.Update.inc(%Update{}, field, value_to_inc)` to inc values `Ravix.RQL.Tokens.Update.dec(%Update{}, field, value_to_dec)` to dec values Returns a `Ravix.RQL.Query` with the update operation ## Examples iex> from = Ravix.RQL.Query.from("cats", "c") iex> update = Ravix.RQL.Query.update(from, set(%Update{}, :cat_name, "Fluffer, the hand-ripper")) """ @spec update(Query.t(), Ravix.RQL.Tokens.Update.t()) :: Query.t() def update(%Query{} = query, update) do %Query{ query | update_token: update } end @doc """ Adds a where operation with a `Ravix.RQL.Tokens.Condition` to the query Returns a `Ravix.RQL.Query` with the where condition ## Examples iex> from = Ravix.RQL.Query.from("cats", "c") iex> where = Ravix.RQL.Query.where(from, equal_to("cat_name", "Meowvius")) """ @spec where(Query.t(), Condition.t()) :: Query.t() def where(%Query{} = query, %Condition{} = condition) do %Query{ query | where_token: Where.condition(condition) } end @doc """ Adds a select operation to project fields Returns a `Ravix.RQL.Query` with the select condition ## Examples iex> from = Ravix.RQL.Query.from("cats", "c") iex> select = Ravix.RQL.Query.select(from, ["name", "breed"]) """ @spec select(Query.t(), Select.allowed_select_params()) :: Query.t() def select(%Query{} = query, fields) do %Query{ query | select_token: Select.fields(fields) } end @doc """ Adds a select operation to project fields, leveraging the use of RavenDB Functions Returns a `Ravix.RQL.Query` with the select condition ## Examples iex> from = Ravix.RQL.Query.from("cats", "c") iex> select = Ravix.RQL.Query.select_function(from, ooga: "c.name") """ @spec select_function(Query.t(), Keyword.t()) :: Query.t() def select_function(%Query{} = query, fields) do %Query{ query | select_token: Select.function(fields) } end @doc """ Adds an `Ravix.RQL.Tokens.And` operation with a `Ravix.RQL.Tokens.Condition` to the query Returns a `Ravix.RQL.Query` with the and condition ## Examples iex> from = Ravix.RQL.Query.from("cats", "c") iex> where = Ravix.RQL.Query.where(from, equal_to("cat_name", "Meowvius")) iex> and_v = Ravix.RQL.Query.and?(where, equal_to("breed", "Fatto")) """ @spec and?(Query.t(), Condition.t()) :: Query.t() def and?(%Query{} = query, %Condition{} = condition) do %Query{ query | and_tokens: query.and_tokens ++ [And.condition(condition)] } end @doc """ Adds an negated `Ravix.RQL.Tokens.And` operation with a `Ravix.RQL.Tokens.Condition` to the query Returns a `Ravix.RQL.Query` with the and condition ## Examples iex> from = Ravix.RQL.Query.from("cats", "c") iex> where = Ravix.RQL.Query.where(from, equal_to("cat_name", "Meowvius")) iex> and_v = Ravix.RQL.Query.and_not(where, equal_to("breed", "Fatto")) """ @spec and_not(Query.t(), Condition.t()) :: Query.t() def and_not(%Query{} = query, %Condition{} = condition) do %Query{ query | and_tokens: query.and_tokens ++ [Not.condition(And.condition(condition))] } end @doc """ Adds an `Ravix.RQL.Tokens.Or` operation with a `Ravix.RQL.Tokens.Condition` to the query Returns a `Ravix.RQL.Query` with the and condition ## Examples iex> from = Ravix.RQL.Query.from("cats", "c") iex> where = Ravix.RQL.Query.where(from, equal_to("cat_name", "Meowvius")) iex> or_v = Ravix.RQL.Query.or?(where, equal_to("breed", "Fatto")) """ @spec or?(Query.t(), Condition.t()) :: Query.t() def or?(%Query{} = query, %Condition{} = condition) do %Query{ query | or_tokens: query.or_tokens ++ [Or.condition(condition)] } end @doc """ Adds a negated `Ravix.RQL.Tokens.Or` operation with a `Ravix.RQL.Tokens.Condition` to the query Returns a `Ravix.RQL.Query` with the and condition ## Examples iex> from = Ravix.RQL.Query.from("cats", "c") iex> where = Ravix.RQL.Query.where(from, equal_to("cat_name", "Meowvius")) iex> or_v = Ravix.RQL.Query.or_not(where, equal_to("breed", "Fatto")) """ @spec or_not(Query.t(), Condition.t()) :: Query.t() def or_not(%Query{} = query, %Condition{} = condition) do %Query{ query | or_tokens: query.and_tokens ++ [Not.condition(Or.condition(condition))] } end @doc """ Adds a `Ravix.RQL.Tokens.Group` operation to the query Returns a `Ravix.RQL.Query` with the group_by condition ## Examples iex> from = Ravix.RQL.Query.from("cats", "c") iex> grouped = Ravix.RQL.Query.group_by(from, "breed") """ @spec group_by(Query.t(), String.t() | [String.t()]) :: Query.t() def group_by(%Query{} = query, fields) do %Query{ query | group_token: Group.by(fields) } end @doc """ Adds a `Ravix.RQL.Tokens.Limit` operation to the query Returns a `Ravix.RQL.Query` with the limit condition ## Examples iex> from = Ravix.RQL.Query.from("cats", "c") iex> limit = Ravix.RQL.Query.limit(from, 5, 10) """ @spec limit(Query.t(), non_neg_integer, non_neg_integer) :: Query.t() def limit(%Query{} = query, skip, next) do %Query{ query | limit_token: Limit.limit(skip, next) } end @doc """ Adds a `Ravix.RQL.Tokens.Order` operation to the query Returns a `Ravix.RQL.Query` with the ordering condition ## Examples iex> from = Ravix.RQL.Query.from("cats", "c") iex> ordered = Ravix.RQL.Query.order_by(from, [{"@metadata.@last-modified", :desc}, {"name", :asc}]) """ @spec order_by( Query.t(), [{:asc, String.t()} | {:desc, String.t()}, ...] | {:asc, String.t()} | {:desc, String.t()} ) :: Query.t() def order_by(%Query{} = query, orders) do %Query{ query | order_token: Order.by(orders) } end @doc """ Create a Query using a raw RQL string Returns a `Ravix.RQL.Query` with the raw query ## Examples iex> raw = Ravix.RQL.Query.raw("from @all_docs where cat_name = \"Fluffers\"") """ @spec raw(String.t()) :: Query.t() def raw(raw_query) do %Query{ query_string: raw_query, is_raw: true } end @doc """ Create a Query using a raw RQL string with replaceable placeholders Returns a `Ravix.RQL.Query` with the raw query and parameters ## Examples iex> raw = Ravix.RQL.Query.raw("from @all_docs where cat_name = $p1", %{p1: "Fluffers"}) """ @spec raw(String.t(), map()) :: Query.t() def raw(raw_query, params) do %Query{ query_string: raw_query, query_params: params, is_raw: true } end @doc """ Executes the query in the informed session and returns the matched documents Returns a [RavenDB response](https://ravendb.net/docs/article-page/4.2/java/client-api/rest-api/queries/query-the-database#response-format) map ## Examples iex> from("Cats") |> select("name") |> where(equal_to("name", cat.name)) |> list_all(session_id) {:ok, %{ "DurationInMs" => 62, "IncludedPaths" => nil, "Includes" => %{}, "IndexName" => "Auto/Cats/By@metadata.@last-modifiedAndidAndname", "IndexTimestamp" => "2022-04-22T20:03:03.8373804", "IsStale" => false, "LastQueryTime" => "2022-04-22T20:03:04.3475275", "LongTotalResults" => 1, "NodeTag" => "A", "ResultEtag" => 6489530344045176783, "Results" => [ %{ "@metadata" => %{ "@change-vector" => "A:6445-HJrwf2z3c0G/FHJPm3zK3w", "@id" => "beee79e2-2560-408c-a680-253e9bd7d12e", "@index-score" => 3.079441547393799, "@last-modified" => "2022-04-22T20:03:03.7477980Z", "@projection" => true }, "name" => "Lily" } ], "ScannedResults" => 0, "SkippedResults" => 0, "TotalResults" => 1 } } """ @spec list_all(Query.t(), binary) :: {:error, any} | {:ok, any} def list_all(%Query{} = query, session_id) do execute_for(query, session_id, "POST", false) end @doc """ Executes the query in the informed session and returns the matched documents Returns a [RavenDB response](https://ravendb.net/docs/article-page/4.2/java/client-api/rest-api/queries/query-the-database#response-format) map ## Examples iex> stream = from("Cats") |> select("name") |> where(equal_to("name", cat.name)) |> stream_all(session_id) stream |> Enum.to_list() [ %{ "@metadata" => %{ "@change-vector" => "A:6445-HJrwf2z3c0G/FHJPm3zK3w", "@id" => "beee79e2-2560-408c-a680-253e9bd7d12e", "@index-score" => 3.079441547393799, "@last-modified" => "2022-04-22T20:03:03.7477980Z", "@projection" => true }, "name" => "Lily" } ] """ @spec stream_all(Ravix.RQL.Query.t(), binary()) :: any def stream_all(%Query{} = query, session_id) do execute_for(query, session_id, "GET", true) end @doc """ Executes the delete query in the informed session Returns a [RavenDB response](https://ravendb.net/docs/article-page/5.3/java/client-api/rest-api/queries/delete-by-query#response-format) map ## Examples iex> from("@all_docs") |> where(equal_to("cat_name", any_entity.cat_name)) |> delete_for(session_id) {:ok, %{"OperationId" => 2480, "OperationNodeTag" => "A"}} """ @spec delete_for(Query.t(), binary) :: {:error, any} | {:ok, any} def delete_for(%Query{} = query, session_id) do execute_for(query, session_id, "DELETE", false) end @doc """ Executes the patch query in the informed session Returns a [RavenDB response](https://ravendb.net/docs/article-page/5.3/java/client-api/rest-api/queries/patch-by-query#response-format) map ## Examples iex> from("@all_docs", "a") |> update(set(%Update{}, :cat_name, "Fluffer, the hand-ripper")) |> where(equal_to("cat_name", any_entity.cat_name)) |> update_for(session_id) {:ok, %{"OperationId" => 2480, "OperationNodeTag" => "A"}} """ @spec update_for(Query.t(), binary) :: {:error, any} | {:ok, any} def update_for(%Query{} = query, session_id) do execute_for(query, session_id, "PATCH", false) end defp execute_for(%Query{is_raw: false} = query, session_id, method, stream) do case QueryParser.parse(query) do {:ok, parsed_query} -> stream_or_list(parsed_query, session_id, method, stream) {:error, err} -> {:error, err} end end defp execute_for(%Query{is_raw: true} = query, session_id, method, stream) do stream_or_list(query, session_id, method, stream) end defp stream_or_list(query, session, method, stream) do case stream do true -> Session.stream_query(query, session) false -> Session.execute_query(query, session, method) end end end