From 3ed2be0409a8637c47e8bceeba3db365e4a19530 Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Thu, 14 Apr 2022 08:07:52 +0200 Subject: [PATCH 01/23] Functions cannot be prepended with the from alias --- lib/rql/query_parser.ex | 9 ++++++++- test/rql/query_parser_test.exs | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/rql/query_parser.ex b/lib/rql/query_parser.ex index c47385c..e5f03b9 100644 --- a/lib/rql/query_parser.ex +++ b/lib/rql/query_parser.ex @@ -132,7 +132,7 @@ defmodule Ravix.RQL.QueryParser do defp parse_select(%Query{} = query, select_token) do query_fragment = - " select " <> Enum.map_join(select_token.fields, ",", &parse_field(query, &1)) + " select " <> Enum.map_join(select_token.fields, ", ", &parse_field(query, &1)) {:ok, append_query_fragment(query, query_fragment)} end @@ -264,6 +264,11 @@ defmodule Ravix.RQL.QueryParser do end end + defp parse_field(%Query{}, {field_name, field_alias}) + when field_name in ["id()", "count()", "sum()"] do + field_name <> " as #{field_alias}" + end + defp parse_field(%Query{aliases: aliases, from_token: from_token}, {field_name, field_alias}) do case Map.has_key?(aliases, from_token.document_or_index) do true -> Map.get(aliases, from_token.document_or_index) <> ".#{field_name} as #{field_alias}" @@ -271,6 +276,8 @@ defmodule Ravix.RQL.QueryParser do end end + defp parse_field(%Query{}, field) when field in ["id()", "count()", "sum()"], do: field + defp parse_field(%Query{aliases: aliases, from_token: from_token}, field) do case Map.has_key?(aliases, from_token.document_or_index) do true -> Map.get(aliases, from_token.document_or_index) <> ".#{field}" diff --git a/test/rql/query_parser_test.exs b/test/rql/query_parser_test.exs index f4de59c..5bffab3 100644 --- a/test/rql/query_parser_test.exs +++ b/test/rql/query_parser_test.exs @@ -59,5 +59,28 @@ defmodule Ravix.RQL.QueryParserTest do assert query_result.params_count == 4 assert query_result.is_raw == false end + + test "Functions should not be prepended with the document alias" do + {:ok, query_result} = + from("test", "t") + |> where(equal_to("id()", "asdf")) + |> or?(equal_to("count()", "asdf")) + |> or?(equal_to("sum()", "asdf")) + |> QueryParser.parse() + + assert query_result.query_string == + "from test as t where id() = $p0 or count() = $p1 or sum() = $p2" + end + + test "Functions can be aliased" do + {:ok, query_result} = + from("test", "t") + |> where(equal_to("id", "asdf")) + |> select([{"id()", "i"}, {"count()", "c"}, {"sum()", "s"}]) + |> QueryParser.parse() + + assert query_result.query_string == + "from test as t where t.id = $p0 select id() as i, count() as c, sum() as s" + end end end From 80f2af12c6606a0428aea903d0c46931e8e4adcc Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Thu, 14 Apr 2022 10:23:55 +0200 Subject: [PATCH 02/23] Ensure_key/1 should support keys that are not strings --- lib/documents/session/session_manager.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/documents/session/session_manager.ex b/lib/documents/session/session_manager.ex index c742702..35f949d 100644 --- a/lib/documents/session/session_manager.ex +++ b/lib/documents/session/session_manager.ex @@ -194,7 +194,7 @@ defmodule Ravix.Documents.Session.Manager do defp ensure_key(nil), do: {:error, :no_valid_id_informed} - defp ensure_key(key) do + defp ensure_key(key) when is_bitstring(key) do key = case String.last(key) do "/" -> "tmp_" <> key <> UUID.uuid4() @@ -204,4 +204,6 @@ defmodule Ravix.Documents.Session.Manager do {:ok, key} end + + defp ensure_key(key), do: {:ok, key} end From f087bac2e680c729824021113ffd0cd5c3794e1b Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Fri, 15 Apr 2022 10:55:19 +0200 Subject: [PATCH 03/23] Fixing document creation when the key is not named :id --- lib/documents/session/models/save_changes_data.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/documents/session/models/save_changes_data.ex b/lib/documents/session/models/save_changes_data.ex index 388d348..0b6fee4 100644 --- a/lib/documents/session/models/save_changes_data.ex +++ b/lib/documents/session/models/save_changes_data.ex @@ -1,6 +1,6 @@ defmodule Ravix.Documents.Session.SaveChangesData do @moduledoc """ - Defines all changes that will be executed in a session when calling the save_changes function + Defines all changes that will be executed in a session when calling the save_changes function ## Fields - deferred_commands_count: How many command will be executed @@ -29,7 +29,7 @@ defmodule Ravix.Documents.Session.SaveChangesData do - deferred_commands: Raven commands to be deferred ## Returns - - Updated `Ravix.Documents.Session.SaveChangesData` + - Updated `Ravix.Documents.Session.SaveChangesData` """ @spec add_deferred_commands(SaveChangesData.t(), list(map())) :: SaveChangesData.t() def add_deferred_commands(%SaveChangesData{} = save_changes_data, deferred_commands) do @@ -48,7 +48,7 @@ defmodule Ravix.Documents.Session.SaveChangesData do - deleted_entities: Entities that will be deleted ## Returns - - Updated `Ravix.Documents.Session.SaveChangesData` + - Updated `Ravix.Documents.Session.SaveChangesData` """ @spec add_delete_commands(SaveChangesData.t(), list(map())) :: SaveChangesData.t() def add_delete_commands(%SaveChangesData{} = save_changes_data, deleted_entities) do @@ -75,7 +75,7 @@ defmodule Ravix.Documents.Session.SaveChangesData do - documents_by_id: Map with the documents that will be created ## Returns - - Updated `Ravix.Documents.Session.SaveChangesData` + - Updated `Ravix.Documents.Session.SaveChangesData` """ @spec add_put_commands(SaveChangesData.t(), map()) :: SaveChangesData.t() def add_put_commands(%SaveChangesData{} = save_changes_data, documents_by_id) do @@ -84,7 +84,7 @@ defmodule Ravix.Documents.Session.SaveChangesData do |> Map.values() |> documents_with_changes() |> Enum.map(fn elmn -> - %PutDocument{Id: elmn.entity.id, Document: elmn.entity} + %PutDocument{Id: elmn.key, Document: elmn.entity} end) entities = put_commands |> Enum.map(fn cmnd -> Map.get(cmnd, "Document") end) From dfd6d54ebbe676de627461a85442487eeb8dbb5e Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Fri, 15 Apr 2022 13:41:26 +0200 Subject: [PATCH 04/23] Added missing retries configuration, added support to retry_on_stale --- config/test.exs | 7 +++- lib/connection/executor/request_executor.ex | 18 ++++++-- .../executor/request_executor_option.ex | 12 ++++++ lib/connection/models/connection_state.ex | 12 ++++++ lib/connection/models/server_node.ex | 10 ++++- test/rql/query_test.exs | 41 ++++++++++++++++++- 6 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 lib/connection/executor/request_executor_option.ex diff --git a/config/test.exs b/config/test.exs index ee7ea30..7cab1ad 100644 --- a/config/test.exs +++ b/config/test.exs @@ -3,7 +3,8 @@ import Config config :ravix, Ravix.TestStore, urls: [System.get_env("RAVENDB_URL", "http://localhost:8080")], database: "test", - retry_on_failure: true, + retry_on_failure: false, + retry_on_stale: false, retry_backoff: 100, retry_count: 3, force_create_database: true, @@ -21,6 +22,7 @@ config :ravix, Ravix.TestStore2, urls: [System.get_env("RAVENDB_URL", "http://localhost:8080")], database: "test2", retry_on_failure: true, + retry_on_stale: true, retry_backoff: 100, retry_count: 3, force_create_database: true, @@ -38,7 +40,8 @@ config :ravix, Ravix.TestStore2, config :ravix, Ravix.TestStoreInvalid, urls: ["http://localhost:9999"], database: "test2", - retry_on_failure: true, + retry_on_failure: false, + retry_on_stale: false, retry_backoff: 100, retry_count: 3, force_create_database: true, diff --git a/lib/connection/executor/request_executor.ex b/lib/connection/executor/request_executor.ex index 7450aab..9b7a6a7 100644 --- a/lib/connection/executor/request_executor.ex +++ b/lib/connection/executor/request_executor.ex @@ -17,6 +17,7 @@ defmodule Ravix.Connection.RequestExecutor do alias Ravix.Connection alias Ravix.Connection.State, as: ConnectionState alias Ravix.Connection.{ServerNode, NodeSelector, Response} + alias Ravix.Connection.RequestExecutor alias Ravix.Documents.Protocols.CreateRequest @doc """ @@ -81,6 +82,7 @@ defmodule Ravix.Connection.RequestExecutor do opts \\ [] ) do node_pid = NodeSelector.current_node(conn_state) + opts = opts ++ RequestExecutor.Options.from_connection_state(conn_state) headers = case conn_state.disable_topology_updates do @@ -120,8 +122,9 @@ defmodule Ravix.Connection.RequestExecutor do end defp call_raven(executor, command, headers, opts) do - should_retry = Keyword.get(opts, :should_retry, false) + should_retry = Keyword.get(opts, :retry_on_failure, false) retry_backoff = Keyword.get(opts, :retry_backoff, 100) + retry_on_stale = Keyword.get(opts, :retry_on_stale, false) retry_count = case should_retry do @@ -130,7 +133,7 @@ defmodule Ravix.Connection.RequestExecutor do end retry with: constant_backoff(retry_backoff) |> Stream.take(retry_count) do - GenServer.call(executor, {:request, command, headers}) + GenServer.call(executor, {:request, command, headers, [retry_on_stale: retry_on_stale]}) after {:ok, result} -> {:ok, result} {:non_retryable_error, response} -> {:error, response} @@ -201,7 +204,7 @@ defmodule Ravix.Connection.RequestExecutor do #################### # Handlers # #################### - def handle_call({:request, command, headers}, from, %ServerNode{} = node) do + def handle_call({:request, command, headers, opts}, from, %ServerNode{} = node) do request = CreateRequest.create_request(command, node) case Mint.HTTP.request( @@ -218,6 +221,7 @@ defmodule Ravix.Connection.RequestExecutor do node = put_in(node.conn, conn) node = put_in(node.requests[request_ref], %{from: from, response: %{}}) + node = put_in(node.opts, opts) {:noreply, node} {:error, conn, reason} -> @@ -287,6 +291,14 @@ defmodule Ravix.Connection.RequestExecutor do %{data: data} when is_map_key(data, "Error") -> {:non_retryable_error, data["Message"]} + %{data: %{"IsStale" => true}} -> + Logger.warn("[RAVIX] The request '#{inspect(request_ref)}' is Stale!") + + case ServerNode.retry_on_stale?(state) do + true -> {:error, :stale} + false -> {:non_retryable_error, :stale} + end + error_response when error_response.status in [408, 502, 503, 504] -> parse_error(error_response) diff --git a/lib/connection/executor/request_executor_option.ex b/lib/connection/executor/request_executor_option.ex new file mode 100644 index 0000000..8d07b4c --- /dev/null +++ b/lib/connection/executor/request_executor_option.ex @@ -0,0 +1,12 @@ +defmodule Ravix.Connection.RequestExecutor.Options do + alias Ravix.Connection.State, as: ConnectionState + + def from_connection_state(%ConnectionState{} = conn_state) do + [ + {:retry_on_failure, conn_state.retry_on_failure}, + {:retry_on_stale, conn_state.retry_on_stale}, + {:retry_backoff, conn_state.retry_backoff}, + {:retry_count, conn_state.retry_count} + ] + end +end diff --git a/lib/connection/models/connection_state.ex b/lib/connection/models/connection_state.ex index 8c46549..095f99b 100644 --- a/lib/connection/models/connection_state.ex +++ b/lib/connection/models/connection_state.ex @@ -7,6 +7,10 @@ defmodule Ravix.Connection.State do - certificate: RavenDB emmited SSL certificate for the database user in base64 - certificate_file: Same as above, but a path to the file in the disk - conventions: Document Configuration conventions + - retry_on_failure: Automatic retry in retryable errors + - retry_on_stale: Automatic retry when the query is stale + - retry_backoff: Amount of time between retries (in ms) + - retry_count: Amount of retries - node_selector: Module that selects the nodes based on different strategies. E.g: Ravix.Connection.NodeSelector - urls: List of the urls of RavenDB servers - topology_etag: ETAG of the RavenDB cluster topology @@ -19,6 +23,10 @@ defmodule Ravix.Connection.State do database: nil, certificate: nil, certificate_file: nil, + retry_on_failure: true, + retry_on_stale: false, + retry_backoff: 100, + retry_count: 3, conventions: %Ravix.Documents.Conventions{}, node_selector: nil, urls: [], @@ -35,6 +43,10 @@ defmodule Ravix.Connection.State do database: String.t(), certificate: String.t() | nil, certificate_file: String.t() | nil, + retry_on_failure: boolean(), + retry_on_stale: boolean(), + retry_backoff: non_neg_integer(), + retry_count: non_neg_integer(), conventions: Ravix.Documents.Conventions.t(), node_selector: Ravix.Connection.NodeSelector.t(), urls: list(String.t()), diff --git a/lib/connection/models/server_node.ex b/lib/connection/models/server_node.ex index 4160d43..0d7c69d 100644 --- a/lib/connection/models/server_node.ex +++ b/lib/connection/models/server_node.ex @@ -11,6 +11,7 @@ defmodule Ravix.Connection.ServerNode do - protocol: http or https - database: For which database is this executor - cluster_tag: Tag of this node in the RavenDB cluster + - opts: General node Options """ defstruct store: nil, url: nil, @@ -20,7 +21,8 @@ defmodule Ravix.Connection.ServerNode do requests: %{}, protocol: nil, database: nil, - cluster_tag: nil + cluster_tag: nil, + opts: [] alias Ravix.Connection.ServerNode @@ -33,7 +35,8 @@ defmodule Ravix.Connection.ServerNode do requests: map(), protocol: atom(), database: String.t(), - cluster_tag: String.t() | nil + cluster_tag: String.t() | nil, + opts: keyword() } @doc """ @@ -75,6 +78,9 @@ defmodule Ravix.Connection.ServerNode do def node_url(%ServerNode{} = server_node), do: "/databases/#{server_node.database}" + @spec retry_on_stale?(ServerNode.t()) :: boolean() + def retry_on_stale?(%ServerNode{} = node), do: Keyword.get(node.opts, :retry_on_stale, false) + defimpl String.Chars, for: Ravix.Connection.ServerNode do def to_string(nil) do "" diff --git a/test/rql/query_test.exs b/test/rql/query_test.exs index 5810dbe..b25b9f9 100644 --- a/test/rql/query_test.exs +++ b/test/rql/query_test.exs @@ -8,6 +8,7 @@ defmodule Ravix.RQL.QueryTest do alias Ravix.Documents.Session alias Ravix.TestStore, as: Store + alias Ravix.TestStore2, as: RetryableStore describe "list_all/2" do test "Should list all the matching documents of a query" do @@ -39,7 +40,7 @@ defmodule Ravix.RQL.QueryTest do test "If no results, it should be a valid response with empty results" do {:ok, response} = OK.for do - session_id <- Store.open_session() + session_id <- RetryableStore.open_session() query_response <- from("@all_docs") @@ -117,6 +118,44 @@ defmodule Ravix.RQL.QueryTest do end end + test "If the query is stale, should return an error" do + cat = build(:cat_entity) + + {:error, :stale} = + OK.for do + session_id <- Store.open_session() + _ <- Session.store(session_id, cat) + _ <- Session.save_changes(session_id) + + query_response <- + from("Cats") + |> select("name") + |> where(equal_to("name", cat.name)) + |> list_all(session_id) + after + query_response + end + end + + test "If the query is stale, but the retry_on_stale is on, it should return ok" do + cat = build(:cat_entity) + + {:ok, _} = + OK.for do + session_id <- RetryableStore.open_session() + _ <- Session.store(session_id, cat) + _ <- Session.save_changes(session_id) + + query_response <- + from("Cats") + |> select("name") + |> where(equal_to("name", cat.name)) + |> list_all(session_id) + after + query_response + end + end + test "Should return only the selected field" do cat = build(:cat_entity) From 69360943cd6ef3c6a63c139e5b9096a18cb7921b Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Sun, 17 Apr 2022 13:08:41 +0200 Subject: [PATCH 05/23] Adding support to set, inc and dec update operations --- README.md | 14 ++++---- config/test.exs | 14 ++++---- docker-compose.yml | 2 +- lib/connection/executor/supervisor.ex | 10 +++--- lib/connection/models/connection_state.ex | 2 +- lib/connection/models/server_node.ex | 2 +- lib/rql/query.ex | 13 +++++-- lib/rql/query_parser.ex | 18 ++++++++-- lib/rql/tokens/update.ex | 36 +++++++++++++++---- test/connection/connection_test.exs | 2 +- test/documents/session/session_test.exs | 4 +-- test/documents/store_test.exs | 2 +- test/operations/database_maintenance_test.exs | 8 ++--- test/rql/query_parser_test.exs | 17 ++++++--- test/rql/query_test.exs | 36 +++++-------------- ...test_store_2.ex => non_retryable_store.ex} | 2 +- test/support/test_application.ex | 4 +-- test/support/test_store.ex | 2 +- test/test_helper.exs | 4 +-- 19 files changed, 111 insertions(+), 81 deletions(-) rename test/support/{test_store_2.ex => non_retryable_store.ex} (53%) diff --git a/README.md b/README.md index c3569ae..e2ada70 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ end You can configure your Store in your config.exs files ```elixir -config :ravix, Ravix.TestStore, +config :ravix, Ravix.Test.Store, urls: [System.get_env("RAVENDB_URL", "http://localhost:8080")], database: "test", retry_on_failure: true, @@ -62,7 +62,7 @@ defmodule Ravix.TestApplication do def init(_opts) do children = [ {Ravix, [%{}]}, - {Ravix.TestStore, [%{}]} # you can create multiple stores + {Ravix.Test.Store, [%{}]} # you can create multiple stores ] Supervisor.init( @@ -87,15 +87,15 @@ All operations supported by the driver should be executed inside a session, to o `Ravix.Documents.Session.save_changes/1` is called! ```elixir -iex(2)> Ravix.TestStore.open_session() +iex(2)> Ravix.Test.Store.open_session() {:ok, "985781c8-9154-494b-92d0-a66b49bb17ee"} ``` ### Inserting a new document ```elixir -iex(2)> Ravix.TestStore.open_session() -iex(2)> {:ok, session_id} = Ravix.TestStore.open_session() +iex(2)> Ravix.Test.Store.open_session() +iex(2)> {:ok, session_id} = Ravix.Test.Store.open_session() {:ok, "c4fb1f48-c969-4c76-9b12-5521926c7533"} iex(3)> Ravix.Documents.Session.store(session_id, %{id: "cat/1", cat_name: "Adolfus"}) {:ok, %{cat_name: "Adolfus", id: "cat/1"}} @@ -117,7 +117,7 @@ iex(4)> Ravix.Documents.Session.save_changes(session_id) ### Loading a document into the session ```elixir -iex(3)> {:ok, session_id} = Ravix.TestStore.open_session() +iex(3)> {:ok, session_id} = Ravix.Test.Store.open_session() {:ok, "d17e2be8-8c1e-4a59-8626-46725387f769"} iex(4)> Ravix.Documents.Session.load(session_id, ["cat/1"]) {:ok, @@ -225,7 +225,7 @@ end To connect to a secure server, you can just inform the SSL certificate using the `certificate` or the `certificate_file` configuration. ```elixir -config :ravix, Ravix.TestStore, +config :ravix, Ravix.Test.Store, urls: [System.get_env("RAVENDB_URL", "http://localhost:8080")], database: "test", certificate: CERT_IN_BASE_64, diff --git a/config/test.exs b/config/test.exs index 7cab1ad..c6b4d46 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,11 +1,11 @@ import Config -config :ravix, Ravix.TestStore, +config :ravix, Ravix.Test.Store, urls: [System.get_env("RAVENDB_URL", "http://localhost:8080")], database: "test", - retry_on_failure: false, - retry_on_stale: false, - retry_backoff: 100, + retry_on_failure: true, + retry_on_stale: true, + retry_backoff: 300, retry_count: 3, force_create_database: true, document_conventions: %{ @@ -18,11 +18,11 @@ config :ravix, Ravix.TestStore, disable_topology_update: false } -config :ravix, Ravix.TestStore2, +config :ravix, Ravix.Test.NonRetryableStore, urls: [System.get_env("RAVENDB_URL", "http://localhost:8080")], database: "test2", - retry_on_failure: true, - retry_on_stale: true, + retry_on_failure: false, + retry_on_stale: false, retry_backoff: 100, retry_count: 3, force_create_database: true, diff --git a/docker-compose.yml b/docker-compose.yml index 1846fb1..a3b41c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.9" services: ravendb: - image: ravendb/ravendb:5.3.1-ubuntu.20.04-arm64v8 + image: ravendb/ravendb:5.3-ubuntu-arm64v8-latest environment: - RAVEN_Setup_Mode=None - RAVEN_License_Eula_Accepted=true diff --git a/lib/connection/executor/supervisor.ex b/lib/connection/executor/supervisor.ex index 7bbfced..c542c4b 100644 --- a/lib/connection/executor/supervisor.ex +++ b/lib/connection/executor/supervisor.ex @@ -3,7 +3,7 @@ defmodule Ravix.Connection.RequestExecutor.Supervisor do Supervises the Requests Executors processes Each node connection has it own supervised process, so they are completely isolated - from each other. All executors are registered under the :request_executors Registry. + from each other. All executors are registered under the :request_executors Registry. """ use DynamicSupervisor @@ -36,7 +36,7 @@ defmodule Ravix.Connection.RequestExecutor.Supervisor do Register a new RavenDB Database node for the informed store ## Parameters - - store: the store module. E.g: Ravix.TestStore + - store: the store module. E.g: Ravix.Test.Store - node: the node to be registered """ @spec register_node_executor(any, ServerNode.t()) :: @@ -54,7 +54,7 @@ defmodule Ravix.Connection.RequestExecutor.Supervisor do Fetches the nodes running for a specific store ## Parameters - - store: the store module: E.g: Ravix.TestStore + - store: the store module: E.g: Ravix.Test.Store ## Returns - List of PIDs @@ -69,8 +69,8 @@ defmodule Ravix.Connection.RequestExecutor.Supervisor do Triggers a topology update for all nodes of a specific store ## Parameters - - store: the store module. E.g: Ravix.TestStore - - topology: The `Ravix.Connection.Topology` to be used + - store: the store module. E.g: Ravix.Test.Store + - topology: The `Ravix.Connection.Topology` to be used ## Returns - List of nodes `[new_nodes: list(Ravix.Connection.ServerNode), updated_nodes: list(Ravix.Connection.ServerNode)]` diff --git a/lib/connection/models/connection_state.ex b/lib/connection/models/connection_state.ex index 095f99b..4fbb870 100644 --- a/lib/connection/models/connection_state.ex +++ b/lib/connection/models/connection_state.ex @@ -2,7 +2,7 @@ defmodule Ravix.Connection.State do @moduledoc """ Represents the state of a RavenDB connection - - store: Store atom for this state. E.g: Ravix.TestStore + - store: Store atom for this state. E.g: Ravix.Test.Store - database: Name of the database. - certificate: RavenDB emmited SSL certificate for the database user in base64 - certificate_file: Same as above, but a path to the file in the disk diff --git a/lib/connection/models/server_node.ex b/lib/connection/models/server_node.ex index 0d7c69d..7b16902 100644 --- a/lib/connection/models/server_node.ex +++ b/lib/connection/models/server_node.ex @@ -2,7 +2,7 @@ defmodule Ravix.Connection.ServerNode do @moduledoc """ State of a RavenDB connection executor node - - store: Atom of the RavenDB Store, E.g: Ravix.TestStore + - store: Atom of the RavenDB Store, E.g: Ravix.Test.Store - url: URL of this node - port: port of this node - conn: TCP Connection State diff --git a/lib/rql/query.ex b/lib/rql/query.ex index 13e3f0d..9cdf38a 100644 --- a/lib/rql/query.ex +++ b/lib/rql/query.ex @@ -80,11 +80,18 @@ defmodule Ravix.RQL.Query do @doc """ Adds a `Ravix.RQL.Tokens.Update` to the query """ - @spec update(Query.t(), map()) :: Query.t() - def update(%Query{} = query, document_updates) do + @spec update(Query.t(), list(Update.Field.t()) | Update.t()) :: Query.t() + def update(%Query{} = query, document_updates) when is_list(document_updates) do %Query{ query - | update_token: Update.update(document_updates) + | update_token: Update.fields(document_updates) + } + end + + def update(%Query{} = query, update) do + %Query{ + query + | update_token: update } end diff --git a/lib/rql/query_parser.ex b/lib/rql/query_parser.ex index e5f03b9..4a5721d 100644 --- a/lib/rql/query_parser.ex +++ b/lib/rql/query_parser.ex @@ -150,16 +150,20 @@ defmodule Ravix.RQL.QueryParser do defp parse_update(%Query{} = query, update_token) do fields_to_update = update_token.fields - |> Map.keys() |> Enum.reduce(%{updates: [], current_position: query.params_count}, fn field, acc -> %{ acc - | updates: acc.updates ++ ["#{parse_field(query, field)} = $p#{acc.current_position}"], + | updates: + acc.updates ++ + [ + "#{parse_field(query, field.name)} #{parse_assignment_operation(field.operation)} $p#{acc.current_position}" + ], current_position: acc.current_position + 1 } end) - positional_params = parse_params_to_positional(query, Map.values(update_token.fields)) + field_values = Enum.map(update_token.fields, fn field -> field.value end) + positional_params = parse_params_to_positional(query, field_values) query_params = Map.merge( @@ -176,6 +180,14 @@ defmodule Ravix.RQL.QueryParser do |> append_query_fragment(" update{ " <> Enum.join(fields_to_update.updates, ", ") <> " }")} end + defp parse_assignment_operation(operation) do + case operation do + :set -> "=" + :inc -> "+=" + :dec -> "-=" + end + end + defp parse_where(%Query{} = query, where_token) do parse_locator_stmt(query, where_token, "where", false) end diff --git a/lib/rql/tokens/update.ex b/lib/rql/tokens/update.ex index ca2fe22..236c738 100644 --- a/lib/rql/tokens/update.ex +++ b/lib/rql/tokens/update.ex @@ -1,21 +1,43 @@ defmodule Ravix.RQL.Tokens.Update do - defstruct [ - :token, - :fields - ] + defstruct token: :update, + fields: [] alias Ravix.RQL.Tokens.Update @type t :: %Update{ token: atom(), - fields: map() + fields: list(map()) } - @spec update(map()) :: Update.t() - def update(fields) do + @spec fields(list(map())) :: Update.t() + def fields(fields) do %Update{ token: :update, fields: fields } end + + @spec set(Update.t(), String.t(), any) :: Ravix.RQL.Tokens.Update.t() + def set(update, field, value) do + %Update{ + update + | fields: update.fields ++ [%{name: field, value: value, operation: :set}] + } + end + + @spec inc(Update.t(), String.t(), any) :: Ravix.RQL.Tokens.Update.t() + def inc(update, field, value) do + %Update{ + update + | fields: update.fields ++ [%{name: field, value: value, operation: :inc}] + } + end + + @spec dec(Update.t(), String.t(), any) :: Ravix.RQL.Tokens.Update.t() + def dec(update, field, value) do + %Update{ + update + | fields: update.fields ++ [%{name: field, value: value, operation: :dec}] + } + end end diff --git a/test/connection/connection_test.exs b/test/connection/connection_test.exs index ec8d050..35ddc40 100644 --- a/test/connection/connection_test.exs +++ b/test/connection/connection_test.exs @@ -2,7 +2,7 @@ defmodule Ravix.Connection.ConnectionTest do use ExUnit.Case alias Ravix.Connection - alias Ravix.TestStore, as: Store + alias Ravix.Test.Store, as: Store alias Ravix.TestStoreInvalid, as: InvalidStore describe "update_topology/1" do diff --git a/test/documents/session/session_test.exs b/test/documents/session/session_test.exs index 1828f56..14c8e69 100644 --- a/test/documents/session/session_test.exs +++ b/test/documents/session/session_test.exs @@ -6,8 +6,8 @@ defmodule Ravix.Documents.SessionTest do require OK alias Ravix.Documents.Session - alias Ravix.TestStore, as: Store - alias Ravix.TestStore2, as: TimedStore + alias Ravix.Test.Store, as: Store + alias Ravix.Test.NonRetryableStore, as: TimedStore setup do %{ravix: start_supervised!(Ravix.TestApplication)} diff --git a/test/documents/store_test.exs b/test/documents/store_test.exs index 91a9b88..46b615c 100644 --- a/test/documents/store_test.exs +++ b/test/documents/store_test.exs @@ -1,7 +1,7 @@ defmodule Ravix.Documents.StoreTest do use ExUnit.Case - alias Ravix.TestStore, as: Store + alias Ravix.Test.Store, as: Store setup do %{ravix: start_supervised!(Ravix.TestApplication)} diff --git a/test/operations/database_maintenance_test.exs b/test/operations/database_maintenance_test.exs index 3547022..d22a5b1 100644 --- a/test/operations/database_maintenance_test.exs +++ b/test/operations/database_maintenance_test.exs @@ -2,7 +2,7 @@ defmodule Ravix.Operations.Database.MaintenanceTest do use ExUnit.Case alias Ravix.Operations.Database.Maintenance - alias Ravix.TestStore2 + alias Ravix.Test.NonRetryableStore setup do %{ravix: start_supervised!(Ravix.TestApplication)} @@ -13,7 +13,7 @@ defmodule Ravix.Operations.Database.MaintenanceTest do test "should create a new database successfully" do db_name = Ravix.Test.Random.safe_random_string(5) - {:ok, created} = Maintenance.create_database(TestStore2, db_name) + {:ok, created} = Maintenance.create_database(NonRetryableStore, db_name) assert created["Name"] == db_name end @@ -21,8 +21,8 @@ defmodule Ravix.Operations.Database.MaintenanceTest do test "If the database already exists, should return an error" do db_name = Ravix.Test.Random.safe_random_string(5) - {:ok, _created} = Maintenance.create_database(TestStore2, db_name) - {:error, err} = Maintenance.create_database(TestStore2, db_name) + {:ok, _created} = Maintenance.create_database(NonRetryableStore, db_name) + {:error, err} = Maintenance.create_database(NonRetryableStore, db_name) assert err == "Database '#{db_name}' already exists!" end diff --git a/test/rql/query_parser_test.exs b/test/rql/query_parser_test.exs index 5bffab3..303b6e1 100644 --- a/test/rql/query_parser_test.exs +++ b/test/rql/query_parser_test.exs @@ -6,6 +6,8 @@ defmodule Ravix.RQL.QueryParserTest do alias Ravix.RQL.QueryParser alias Ravix.RQL.Tokens.Condition + alias Ravix.RQL.Tokens.Update + describe "parse/1" do test "It should parse the tokens succesfully" do @@ -41,22 +43,29 @@ defmodule Ravix.RQL.QueryParserTest do end test "Should parse an update succesfully" do + updates = [ + %{name: "field", value: "new_value", operation: :set}, + %{name: "field2", value: 1, operation: :inc}, + %{name: "field3", value: 2, operation: :dec} + ] + {:ok, query_result} = from("test", "t") |> where(greater_than("field", 10)) |> and?(equal_to("field2", "asdf")) - |> update(%{field: "new_value", field2: "new_value_2"}) + |> update(Update.fields(updates)) |> QueryParser.parse() assert query_result.query_string == - "from test as t where t.field > $p0 and t.field2 = $p1 update{ t.field = $p2, t.field2 = $p3 }" + "from test as t where t.field > $p0 and t.field2 = $p1 update{ t.field = $p2, t.field2 += $p3, t.field3 -= $p4 }" assert query_result.query_params["p0"] == 10 assert query_result.query_params["p1"] == "asdf" assert query_result.query_params["p2"] == "new_value" - assert query_result.query_params["p3"] == "new_value_2" + assert query_result.query_params["p3"] == 1 + assert query_result.query_params["p4"] == 2 - assert query_result.params_count == 4 + assert query_result.params_count == 5 assert query_result.is_raw == false end diff --git a/test/rql/query_test.exs b/test/rql/query_test.exs index b25b9f9..f47e34c 100644 --- a/test/rql/query_test.exs +++ b/test/rql/query_test.exs @@ -4,11 +4,13 @@ defmodule Ravix.RQL.QueryTest do import Ravix.RQL.Query import Ravix.RQL.Tokens.Condition + import Ravix.RQL.Tokens.Update import Ravix.Factory alias Ravix.Documents.Session - alias Ravix.TestStore, as: Store - alias Ravix.TestStore2, as: RetryableStore + alias Ravix.RQL.Tokens.Update + alias Ravix.Test.Store, as: Store + alias Ravix.Test.NonRetryableStore describe "list_all/2" do test "Should list all the matching documents of a query" do @@ -40,7 +42,7 @@ defmodule Ravix.RQL.QueryTest do test "If no results, it should be a valid response with empty results" do {:ok, response} = OK.for do - session_id <- RetryableStore.open_session() + session_id <- Store.open_session() query_response <- from("@all_docs") @@ -64,8 +66,6 @@ defmodule Ravix.RQL.QueryTest do _ <- Session.store(session_id, any_entity) _ <- Session.save_changes(session_id) - :timer.sleep(500) - query_response <- raw("from @all_docs where cat_name = \"#{any_entity.cat_name}\"") |> list_all(session_id) @@ -89,8 +89,6 @@ defmodule Ravix.RQL.QueryTest do _ <- Session.store(session_id, any_entity) _ <- Session.save_changes(session_id) - :timer.sleep(500) - query_response <- raw("from @all_docs where cat_name = $p1", %{p1: any_entity.cat_name}) |> list_all(session_id) @@ -123,7 +121,7 @@ defmodule Ravix.RQL.QueryTest do {:error, :stale} = OK.for do - session_id <- Store.open_session() + session_id <- NonRetryableStore.open_session() _ <- Session.store(session_id, cat) _ <- Session.save_changes(session_id) @@ -142,7 +140,7 @@ defmodule Ravix.RQL.QueryTest do {:ok, _} = OK.for do - session_id <- RetryableStore.open_session() + session_id <- Store.open_session() _ <- Session.store(session_id, cat) _ <- Session.save_changes(session_id) @@ -165,8 +163,6 @@ defmodule Ravix.RQL.QueryTest do _ <- Session.store(session_id, cat) _ <- Session.save_changes(session_id) - :timer.sleep(500) - query_response <- from("Cats") |> select("name") @@ -192,8 +188,6 @@ defmodule Ravix.RQL.QueryTest do _ <- Session.store(session_id, cat) _ <- Session.save_changes(session_id) - :timer.sleep(500) - query_response <- from("Cats") |> select(["name", "breed"]) @@ -220,8 +214,6 @@ defmodule Ravix.RQL.QueryTest do _ <- Session.store(session_id, cat) _ <- Session.save_changes(session_id) - :timer.sleep(500) - query_response <- from("Cats") |> select({"name", "cat_name"}) @@ -282,8 +274,6 @@ defmodule Ravix.RQL.QueryTest do _ <- Session.store(session_id, cat3) _ <- Session.save_changes(session_id) - :timer.sleep(500) - query_response <- from("Cats") |> where(in?("name", [cat1.name, cat2.name, cat3.name])) @@ -306,8 +296,6 @@ defmodule Ravix.RQL.QueryTest do _ <- Session.store(session_id, cat) _ <- Session.save_changes(session_id) - :timer.sleep(500) - query_response <- from("Cats", "c") |> where(equal_to("id", cat.id)) @@ -330,8 +318,6 @@ defmodule Ravix.RQL.QueryTest do _ <- Session.store(session_id, cat) _ <- Session.save_changes(session_id) - :timer.sleep(500) - query_response <- from("Cats") |> group_by("breed") @@ -361,8 +347,6 @@ defmodule Ravix.RQL.QueryTest do _ <- Session.save_changes(session_id) - :timer.sleep(500) - query_response <- from("Cats") |> limit(1, 2) @@ -383,8 +367,6 @@ defmodule Ravix.RQL.QueryTest do _ <- Session.store(session_id, el_cato) _ <- Session.save_changes(session_id) - :timer.sleep(500) - query_response <- from("Cats") |> where(not_in("id", [el_cato.id])) @@ -407,8 +389,6 @@ defmodule Ravix.RQL.QueryTest do _ <- Session.store(session_id, cat3) _ <- Session.save_changes(session_id) - :timer.sleep(500) - query_response <- from("Cats") |> where(not_equal_to("id", cat1.id)) @@ -474,7 +454,7 @@ defmodule Ravix.RQL.QueryTest do update_response <- from("@all_docs", "a") - |> update(%{cat_name: "Fluffer, the hand-ripper"}) + |> update(set(%Update{}, :cat_name, "Fluffer, the hand-ripper")) |> where(equal_to("cat_name", any_entity.cat_name)) |> update_for(session_id) diff --git a/test/support/test_store_2.ex b/test/support/non_retryable_store.ex similarity index 53% rename from test/support/test_store_2.ex rename to test/support/non_retryable_store.ex index bf4f663..c57c80b 100644 --- a/test/support/test_store_2.ex +++ b/test/support/non_retryable_store.ex @@ -1,3 +1,3 @@ -defmodule Ravix.TestStore2 do +defmodule Ravix.Test.NonRetryableStore do use Ravix.Documents.Store, otp_app: :ravix end diff --git a/test/support/test_application.ex b/test/support/test_application.ex index 8506493..fd46206 100644 --- a/test/support/test_application.ex +++ b/test/support/test_application.ex @@ -4,8 +4,8 @@ defmodule Ravix.TestApplication do def init(_opts) do children = [ {Ravix, [%{}]}, - {Ravix.TestStore, [%{}]}, - {Ravix.TestStore2, [%{}]} + {Ravix.Test.Store, [%{}]}, + {Ravix.Test.NonRetryableStore, [%{}]} ] Supervisor.init( diff --git a/test/support/test_store.ex b/test/support/test_store.ex index e73cc67..4151fe8 100644 --- a/test/support/test_store.ex +++ b/test/support/test_store.ex @@ -1,3 +1,3 @@ -defmodule Ravix.TestStore do +defmodule Ravix.Test.Store do use Ravix.Documents.Store, otp_app: :ravix end diff --git a/test/test_helper.exs b/test/test_helper.exs index 4958caf..d216946 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -11,9 +11,9 @@ defmodule Ravix.Integration.Case do setup do _ = start_supervised!(Ravix.TestApplication) - {:ok, session_id} = Ravix.TestStore.open_session() + {:ok, session_id} = Ravix.Test.Store.open_session() {:ok, _} = from("@all_docs") |> delete_for(session_id) - _ = Ravix.TestStore.close_session(session_id) + _ = Ravix.Test.Store.close_session(session_id) :ok end From bc9e0dc624a41286313539ea14d126d60d654845 Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Sun, 17 Apr 2022 19:50:47 +0200 Subject: [PATCH 06/23] Fixing dialyzer specs --- lib/rql/query.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/rql/query.ex b/lib/rql/query.ex index 9cdf38a..7460189 100644 --- a/lib/rql/query.ex +++ b/lib/rql/query.ex @@ -58,7 +58,7 @@ defmodule Ravix.RQL.Query do @doc """ Adds a `Ravix.RQL.Tokens.From` to the query """ - @spec from(nil | binary()) :: {:error, :query_document_must_be_informed} | Query.t() + @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 @@ -80,7 +80,7 @@ defmodule Ravix.RQL.Query do @doc """ Adds a `Ravix.RQL.Tokens.Update` to the query """ - @spec update(Query.t(), list(Update.Field.t()) | Update.t()) :: Query.t() + @spec update(Query.t(), list(map()) | Update.t()) :: Query.t() def update(%Query{} = query, document_updates) when is_list(document_updates) do %Query{ query @@ -152,6 +152,7 @@ defmodule Ravix.RQL.Query do } end + @spec group_by(Query.t(), String.t() | [String.t()]) :: Query.t() def group_by(%Query{} = query, fields) do %Query{ query From a273fa7eb40dbd86b00bc8f66d9d921568ccc467 Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Mon, 18 Apr 2022 12:58:55 +0200 Subject: [PATCH 07/23] Fixing order by parser, fixing stale test --- lib/rql/query_parser.ex | 2 +- test/rql/query_test.exs | 8 ++++++++ test/test_helper.exs | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/rql/query_parser.ex b/lib/rql/query_parser.ex index 4a5721d..005aa12 100644 --- a/lib/rql/query_parser.ex +++ b/lib/rql/query_parser.ex @@ -19,9 +19,9 @@ defmodule Ravix.RQL.QueryParser do |> parse_stmt(query.group_token) |> parse_stmts(query.and_tokens) |> parse_stmts(query.or_tokens) + |> parse_stmt(query.order_token) |> parse_stmt(query.update_token) |> parse_stmt(query.select_token) - |> parse_stmt(query.order_token) |> parse_stmt(query.limit_token) after parsed_query diff --git a/test/rql/query_test.exs b/test/rql/query_test.exs index f47e34c..90c66fd 100644 --- a/test/rql/query_test.exs +++ b/test/rql/query_test.exs @@ -125,6 +125,14 @@ defmodule Ravix.RQL.QueryTest do _ <- Session.store(session_id, cat) _ <- Session.save_changes(session_id) + _ <- + from("Cats") + |> select("name") + |> where(equal_to("name", cat.name)) + |> list_all(session_id) + + # The first query usually creates an auto_index, who gives time to the query to finish + # If we query again, it will be faster, leading to a stale call query_response <- from("Cats") |> select("name") diff --git a/test/test_helper.exs b/test/test_helper.exs index d216946..597fb95 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,6 +1,7 @@ {:ok, _} = Application.ensure_all_started(:ex_machina) Faker.start() + ExUnit.start() defmodule Ravix.Integration.Case do From 4db1647b9a98db8d83aa3eecb8b1ea51023e55ad Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Mon, 18 Apr 2022 14:50:41 +0200 Subject: [PATCH 08/23] Fixing parser ordering, disabling flaky stale test --- lib/rql/query_parser.ex | 2 +- test/rql/query_test.exs | 1 + test/test_helper.exs | 6 +++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/rql/query_parser.ex b/lib/rql/query_parser.ex index 005aa12..020ce8e 100644 --- a/lib/rql/query_parser.ex +++ b/lib/rql/query_parser.ex @@ -16,10 +16,10 @@ defmodule Ravix.RQL.QueryParser do query |> parse_stmt(query.from_token) |> parse_stmt(query.where_token) + |> parse_stmt(query.order_token) |> parse_stmt(query.group_token) |> parse_stmts(query.and_tokens) |> parse_stmts(query.or_tokens) - |> parse_stmt(query.order_token) |> parse_stmt(query.update_token) |> parse_stmt(query.select_token) |> parse_stmt(query.limit_token) diff --git a/test/rql/query_test.exs b/test/rql/query_test.exs index 90c66fd..532e51e 100644 --- a/test/rql/query_test.exs +++ b/test/rql/query_test.exs @@ -116,6 +116,7 @@ defmodule Ravix.RQL.QueryTest do end end + @tag :flaky test "If the query is stale, should return an error" do cat = build(:cat_entity) diff --git a/test/test_helper.exs b/test/test_helper.exs index 597fb95..f0d8af1 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -2,7 +2,11 @@ Faker.start() -ExUnit.start() +ExUnit.start( + exclude: [ + :flaky + ] +) defmodule Ravix.Integration.Case do use ExUnit.CaseTemplate From 8e16eed5d57a7e4d459e103d8606a9919c5871cd Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Mon, 18 Apr 2022 14:58:31 +0200 Subject: [PATCH 09/23] Fixing group by test --- lib/rql/query_parser.ex | 2 +- test/rql/query_test.exs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/rql/query_parser.ex b/lib/rql/query_parser.ex index 020ce8e..98e5a81 100644 --- a/lib/rql/query_parser.ex +++ b/lib/rql/query_parser.ex @@ -15,9 +15,9 @@ defmodule Ravix.RQL.QueryParser do parsed_query = query |> parse_stmt(query.from_token) + |> parse_stmt(query.group_token) |> parse_stmt(query.where_token) |> parse_stmt(query.order_token) - |> parse_stmt(query.group_token) |> parse_stmts(query.and_tokens) |> parse_stmts(query.or_tokens) |> parse_stmt(query.update_token) diff --git a/test/rql/query_test.exs b/test/rql/query_test.exs index 532e51e..47afd3c 100644 --- a/test/rql/query_test.exs +++ b/test/rql/query_test.exs @@ -330,6 +330,7 @@ defmodule Ravix.RQL.QueryTest do query_response <- from("Cats") |> group_by("breed") + |> where(equal_to("breed", cat.breed)) |> list_all(session_id) after query_response From 7b4c76431875fda7c7227d3c755d259eb3bcada3 Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Thu, 21 Apr 2022 15:03:40 +0200 Subject: [PATCH 10/23] Adding atoms to non aliasable fields --- lib/rql/query_parser.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/rql/query_parser.ex b/lib/rql/query_parser.ex index 98e5a81..7a3ff0f 100644 --- a/lib/rql/query_parser.ex +++ b/lib/rql/query_parser.ex @@ -6,6 +6,8 @@ defmodule Ravix.RQL.QueryParser do alias Ravix.RQL.Query + @non_aliasable_fields ["id()", "count()", "sum()", :"id()", :"count()", :"sum()"] + @doc """ Receives a `Ravix.RQL.Query` object and parses it to a RQL query string """ @@ -277,7 +279,7 @@ defmodule Ravix.RQL.QueryParser do end defp parse_field(%Query{}, {field_name, field_alias}) - when field_name in ["id()", "count()", "sum()"] do + when field_name in @non_aliasable_fields do field_name <> " as #{field_alias}" end @@ -288,7 +290,7 @@ defmodule Ravix.RQL.QueryParser do end end - defp parse_field(%Query{}, field) when field in ["id()", "count()", "sum()"], do: field + defp parse_field(%Query{}, field) when field in @non_aliasable_fields, do: field defp parse_field(%Query{aliases: aliases, from_token: from_token}, field) do case Map.has_key?(aliases, from_token.document_or_index) do From 0b43fae6f9a23d5f06b5d67f1c88ea8edd597788 Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Fri, 22 Apr 2022 14:49:42 +0200 Subject: [PATCH 11/23] Implementing some validations --- config/test.exs | 21 ++++++- lib/connection/executor/request_executor.ex | 57 ++++++++++++------- .../executor/request_executor_option.ex | 4 +- .../commands/data/delete_documents.ex | 4 +- lib/documents/commands/data/put_documents.ex | 4 +- .../session/models/save_changes_data.ex | 4 +- lib/documents/session/session_manager.ex | 14 ++++- lib/documents/session/validations.ex | 39 ++++++++++--- test/documents/session/session_test.exs | 34 +++++++++++ test/support/optimistic_lock_store.ex | 3 + test/support/test_application.ex | 3 +- 11 files changed, 147 insertions(+), 40 deletions(-) create mode 100644 test/support/optimistic_lock_store.ex diff --git a/config/test.exs b/config/test.exs index c6b4d46..8305bcd 100644 --- a/config/test.exs +++ b/config/test.exs @@ -5,7 +5,7 @@ config :ravix, Ravix.Test.Store, database: "test", retry_on_failure: true, retry_on_stale: true, - retry_backoff: 300, + retry_backoff: 500, retry_count: 3, force_create_database: true, document_conventions: %{ @@ -37,6 +37,25 @@ config :ravix, Ravix.Test.NonRetryableStore, disable_topology_update: false } +config :ravix, Ravix.Test.OptimisticLockStore, + urls: [System.get_env("RAVENDB_URL", "http://localhost:8080")], + database: "test3", + retry_on_failure: false, + retry_on_stale: false, + retry_backoff: 100, + retry_count: 3, + force_create_database: true, + document_conventions: %{ + max_number_of_requests_per_session: 2, + max_ids_to_catch: 4, + timeout: 30, + session_idle_ttl: 3, + use_optimistic_concurrency: true, + max_length_of_query_using_get_url: 50, + identity_parts_separator: "/", + disable_topology_update: false + } + config :ravix, Ravix.TestStoreInvalid, urls: ["http://localhost:9999"], database: "test2", diff --git a/lib/connection/executor/request_executor.ex b/lib/connection/executor/request_executor.ex index 9b7a6a7..85efd18 100644 --- a/lib/connection/executor/request_executor.ex +++ b/lib/connection/executor/request_executor.ex @@ -124,7 +124,6 @@ defmodule Ravix.Connection.RequestExecutor do defp call_raven(executor, command, headers, opts) do should_retry = Keyword.get(opts, :retry_on_failure, false) retry_backoff = Keyword.get(opts, :retry_backoff, 100) - retry_on_stale = Keyword.get(opts, :retry_on_stale, false) retry_count = case should_retry do @@ -133,7 +132,7 @@ defmodule Ravix.Connection.RequestExecutor do end retry with: constant_backoff(retry_backoff) |> Stream.take(retry_count) do - GenServer.call(executor, {:request, command, headers, [retry_on_stale: retry_on_stale]}) + GenServer.call(executor, {:request, command, headers, opts}) after {:ok, result} -> {:ok, result} {:non_retryable_error, response} -> {:error, response} @@ -207,26 +206,9 @@ defmodule Ravix.Connection.RequestExecutor do def handle_call({:request, command, headers, opts}, from, %ServerNode{} = node) do request = CreateRequest.create_request(command, node) - case Mint.HTTP.request( - node.conn, - request.method, - request.url, - @default_headers ++ [headers], - request.data - ) do - {:ok, conn, request_ref} -> - Logger.debug( - "[RAVIX] Executing command #{inspect(command)} under the request '#{inspect(request_ref)}' for the store #{inspect(node.store)}" - ) - - node = put_in(node.conn, conn) - node = put_in(node.requests[request_ref], %{from: from, response: %{}}) - node = put_in(node.opts, opts) - {:noreply, node} - - {:error, conn, reason} -> - state = put_in(node.conn, conn) - {:reply, {:error, reason}, state} + case maximum_url_length_reached?(opts, request.url) do + true -> {:reply, {:error, :maximum_url_length_reached}, node} + false -> exec_request(node, from, request, command, headers, opts) end end @@ -258,6 +240,30 @@ defmodule Ravix.Connection.RequestExecutor do {:noreply, %ServerNode{node | cluster_tag: cluster_tag}} end + defp exec_request(%ServerNode{} = node, from, request, command, headers, opts) do + case Mint.HTTP.request( + node.conn, + request.method, + request.url, + @default_headers ++ [headers], + request.data + ) do + {:ok, conn, request_ref} -> + Logger.debug( + "[RAVIX] Executing command #{inspect(command)} under the request '#{inspect(request_ref)}' for the store #{inspect(node.store)}" + ) + + node = put_in(node.conn, conn) + node = put_in(node.requests[request_ref], %{from: from, response: %{}}) + node = put_in(node.opts, opts) + {:noreply, node} + + {:error, conn, reason} -> + state = put_in(node.conn, conn) + {:reply, {:error, reason}, state} + end + end + defp process_response({:status, request_ref, status}, state) do put_in(state.requests[request_ref].response[:status], status) end @@ -366,4 +372,11 @@ defmodule Ravix.Connection.RequestExecutor do end defp decode_body(_), do: nil + + @spec maximum_url_length_reached?(keyword(), String.t()) :: boolean() + defp maximum_url_length_reached?(opts, url) do + max_url_length = Keyword.get(opts, :max_length_of_query_using_get_url, 1024 + 512) + + String.length(url) > max_url_length + end end diff --git a/lib/connection/executor/request_executor_option.ex b/lib/connection/executor/request_executor_option.ex index 8d07b4c..5ffee75 100644 --- a/lib/connection/executor/request_executor_option.ex +++ b/lib/connection/executor/request_executor_option.ex @@ -6,7 +6,9 @@ defmodule Ravix.Connection.RequestExecutor.Options do {:retry_on_failure, conn_state.retry_on_failure}, {:retry_on_stale, conn_state.retry_on_stale}, {:retry_backoff, conn_state.retry_backoff}, - {:retry_count, conn_state.retry_count} + {:retry_count, conn_state.retry_count}, + {:max_length_of_query_using_get_url, + conn_state.conventions.max_length_of_query_using_get_url} ] end end diff --git a/lib/documents/commands/data/delete_documents.ex b/lib/documents/commands/data/delete_documents.ex index e04a77e..a1b9b3a 100644 --- a/lib/documents/commands/data/delete_documents.ex +++ b/lib/documents/commands/data/delete_documents.ex @@ -1,12 +1,14 @@ defmodule Ravix.Documents.Commands.Data.DeleteDocument do - @derive {Jason.Encoder, only: [:Id, :Type]} + @derive {Jason.Encoder, only: [:Id, :ChangeVector, :Type]} defstruct Id: nil, + ChangeVector: nil, Type: "DELETE" alias Ravix.Documents.Commands.Data.DeleteDocument @type t :: %DeleteDocument{ Id: binary(), + ChangeVector: String.t(), Type: String.t() } end diff --git a/lib/documents/commands/data/put_documents.ex b/lib/documents/commands/data/put_documents.ex index 9e28509..79cba02 100644 --- a/lib/documents/commands/data/put_documents.ex +++ b/lib/documents/commands/data/put_documents.ex @@ -1,7 +1,8 @@ defmodule Ravix.Documents.Commands.Data.PutDocument do - @derive {Jason.Encoder, only: [:Id, :Document, :Type]} + @derive {Jason.Encoder, only: [:Id, :Document, :ChangeVector, :Type]} defstruct Id: nil, Document: nil, + ChangeVector: nil, Type: "PUT" alias Ravix.Documents.Commands.Data.PutDocument @@ -9,6 +10,7 @@ defmodule Ravix.Documents.Commands.Data.PutDocument do @type t :: %PutDocument{ Id: binary(), Document: map(), + ChangeVector: String.t(), Type: String.t() } end diff --git a/lib/documents/session/models/save_changes_data.ex b/lib/documents/session/models/save_changes_data.ex index 0b6fee4..5c5e2d6 100644 --- a/lib/documents/session/models/save_changes_data.ex +++ b/lib/documents/session/models/save_changes_data.ex @@ -56,7 +56,7 @@ defmodule Ravix.Documents.Session.SaveChangesData do Enum.map( deleted_entities, fn entity -> - %DeleteDocument{Id: entity.key} + %DeleteDocument{Id: entity.key, ChangeVector: entity.change_vector} end ) @@ -84,7 +84,7 @@ defmodule Ravix.Documents.Session.SaveChangesData do |> Map.values() |> documents_with_changes() |> Enum.map(fn elmn -> - %PutDocument{Id: elmn.key, Document: elmn.entity} + %PutDocument{Id: elmn.key, Document: elmn.entity, ChangeVector: elmn.change_vector} end) entities = put_commands |> Enum.map(fn cmnd -> Map.get(cmnd, "Document") end) diff --git a/lib/documents/session/session_manager.ex b/lib/documents/session/session_manager.ex index 35f949d..d1cf164 100644 --- a/lib/documents/session/session_manager.ex +++ b/lib/documents/session/session_manager.ex @@ -20,6 +20,7 @@ defmodule Ravix.Documents.Session.Manager do def load_documents(%SessionState{} = state, document_ids, includes, opts) do OK.try do + _ <- Validations.load_documents_limit_reached(state, document_ids) already_loaded_ids = fetch_loaded_documents(state, document_ids) ids_to_load <- Validations.all_ids_are_not_already_loaded(document_ids, already_loaded_ids) network_state <- Connection.fetch_state(state.store) @@ -65,6 +66,14 @@ defmodule Ravix.Documents.Session.Manager do defp do_store_entity(entity, key, change_vector, %SessionState{} = state) do OK.try do + _ <- Validations.session_request_limit_reached(state) + + change_vector = + case state.conventions.use_optimistic_concurrency do + true -> change_vector + false -> nil + end + local_key <- ensure_key(key) metadata = Metadata.build_default_metadata(entity) entity = Metadata.add_metadata(entity, metadata) @@ -72,6 +81,7 @@ defmodule Ravix.Documents.Session.Manager do updated_state <- state + |> SessionState.increment_request_count() |> SessionState.update_last_session_call() |> SessionState.register_document(local_key, entity, change_vector, original_document) after @@ -109,6 +119,7 @@ defmodule Ravix.Documents.Session.Manager do OK.for do updated_state <- state + |> SessionState.increment_request_count() |> SessionState.update_last_session_call() |> SessionState.mark_document_for_exclusion(document_id) after @@ -142,7 +153,7 @@ defmodule Ravix.Documents.Session.Manager do {:error, {:document_already_stored, stored_document}} -> stored_document.key end end) - |> Enum.reject(fn item -> item == nil end) + |> Enum.reject(&is_nil/1) end defp execute_load_request(%ConnectionState{} = network_state, ids, includes, opts) @@ -182,7 +193,6 @@ defmodule Ravix.Documents.Session.Manager do updated_state = state - |> SessionState.increment_request_count() |> SessionState.update_last_session_call() |> SessionState.clear_deferred_commands() |> SessionState.clear_deleted_entities() diff --git a/lib/documents/session/validations.ex b/lib/documents/session/validations.ex index 21b1b29..91eb11a 100644 --- a/lib/documents/session/validations.ex +++ b/lib/documents/session/validations.ex @@ -2,14 +2,14 @@ defmodule Ravix.Documents.Session.Validations do @moduledoc """ Validation rules for session states """ - alias Ravix.Documents.Session.State + alias Ravix.Documents.Session.State, as: SessionState @doc """ Returns an error if the document is in a deferred command """ - @spec document_not_in_deferred_command(State.t(), binary()) :: + @spec document_not_in_deferred_command(SessionState.t(), binary()) :: {:ok, binary()} | {:error, :document_already_deferred} - def document_not_in_deferred_command(%State{} = state, entity_id) do + def document_not_in_deferred_command(%SessionState{} = state, entity_id) do state.defer_commands |> Enum.find_value({:ok, entity_id}, fn elmn -> if elmn.key == entity_id, do: {:error, :document_already_deferred} @@ -19,8 +19,9 @@ defmodule Ravix.Documents.Session.Validations do @doc """ Returns an error if the document is already marked for deletion """ - @spec document_not_deleted(State.t(), binary()) :: {:ok, binary()} | {:error, :document_deleted} - def document_not_deleted(%State{} = state, entity_id) do + @spec document_not_deleted(SessionState.t(), binary()) :: + {:ok, binary()} | {:error, :document_deleted} + def document_not_deleted(%SessionState{} = state, entity_id) do state.deleted_entities |> Enum.find_value({:ok, entity_id}, fn elmn -> if elmn.key == entity_id, do: {:error, :document_deleted} @@ -30,9 +31,9 @@ defmodule Ravix.Documents.Session.Validations do @doc """ Returns an error if the document is already stored in the session """ - @spec document_not_stored(State.t(), binary()) :: + @spec document_not_stored(SessionState.t(), binary()) :: {:error, {:document_already_stored, map()}} | {:ok, map()} - def document_not_stored(%State{} = state, entity_id) do + def document_not_stored(%SessionState{} = state, entity_id) do case Map.get(state.documents_by_id, entity_id) do nil -> {:ok, entity_id} document -> {:error, {:document_already_stored, document}} @@ -42,8 +43,9 @@ defmodule Ravix.Documents.Session.Validations do @doc """ Return an error if the document is not in the session """ - @spec document_in_session?(State.t(), any) :: {:error, :document_not_in_session} | {:ok, map} - def document_in_session?(%State{} = state, entity_id) do + @spec document_in_session?(SessionState.t(), any) :: + {:error, :document_not_in_session} | {:ok, map} + def document_in_session?(%SessionState{} = state, entity_id) do case Map.get(state.documents_by_id, entity_id) do nil -> {:error, :document_not_in_session} document -> {:ok, document} @@ -61,4 +63,23 @@ defmodule Ravix.Documents.Session.Validations do ids_to_load -> {:ok, ids_to_load} end end + + @spec session_request_limit_reached(SessionState.t()) :: + {:error, :max_amount_of_requests_reached} | {:ok, -1} + def session_request_limit_reached(%SessionState{} = state) do + case state.number_of_requests + 1 > state.conventions.max_number_of_requests_per_session do + false -> {:ok, -1} + true -> {:error, :max_amount_of_requests_reached} + end + end + + @spec load_documents_limit_reached(SessionState.t(), list) :: + {:error, :max_amount_of_ids_reached} | {:ok, -1} + def load_documents_limit_reached(%SessionState{} = state, document_ids) do + case (Map.keys(state.documents_by_id) |> length) + length(document_ids) > + state.conventions.max_ids_to_catch do + false -> {:ok, -1} + true -> {:error, :max_amount_of_ids_reached} + end + end end diff --git a/test/documents/session/session_test.exs b/test/documents/session/session_test.exs index 14c8e69..466e513 100644 --- a/test/documents/session/session_test.exs +++ b/test/documents/session/session_test.exs @@ -8,6 +8,7 @@ defmodule Ravix.Documents.SessionTest do alias Ravix.Documents.Session alias Ravix.Test.Store, as: Store alias Ravix.Test.NonRetryableStore, as: TimedStore + alias Ravix.Test.OptimisticLockStore, as: OptimisticStore setup do %{ravix: start_supervised!(Ravix.TestApplication)} @@ -100,6 +101,21 @@ defmodule Ravix.Documents.SessionTest do after end end + + test "If the amount of operations exceed the limit, an error should be returned" do + any_entity = %{id: UUID.uuid4(), cat_name: Faker.Cat.name()} + any_entity_2 = %{id: UUID.uuid4(), cat_name: Faker.Cat.name()} + any_entity_3 = %{id: UUID.uuid4(), cat_name: Faker.Cat.name()} + + {:error, :max_amount_of_requests_reached} = + OK.for do + session_id <- OptimisticStore.open_session() + _ <- Session.store(session_id, any_entity) + _ <- Session.store(session_id, any_entity_2) + _ <- Session.store(session_id, any_entity_3) + after + end + end end describe "save_changes/1" do @@ -284,6 +300,15 @@ defmodule Ravix.Documents.SessionTest do assert Map.has_key?(state.documents_by_id, any_entity.id) end + test "If the url size reaches the limit, it should return an error" do + {:error, :maximum_url_length_reached} = + OK.for do + session_id <- OptimisticStore.open_session() + _ <- Session.load(session_id, [Ravix.Test.Random.safe_random_string(50)]) + after + end + end + test "Includes should be loaded successfully" do any_entity_to_include = %{id: UUID.uuid4(), owner_name: Faker.Person.first_name()} @@ -320,6 +345,15 @@ defmodule Ravix.Documents.SessionTest do assert state.documents_by_id[any_entity.id].entity == any_entity |> Morphix.stringmorphiform!() end + + test "If the amount loaded ids exceed the limit, it should return an error" do + {:error, :max_amount_of_ids_reached} = + OK.for do + session_id <- OptimisticStore.open_session() + _ <- Session.load(session_id, ["1", "2", "3", "4", "5"]) + after + end + end end describe "delete/1" do diff --git a/test/support/optimistic_lock_store.ex b/test/support/optimistic_lock_store.ex new file mode 100644 index 0000000..a279538 --- /dev/null +++ b/test/support/optimistic_lock_store.ex @@ -0,0 +1,3 @@ +defmodule Ravix.Test.OptimisticLockStore do + use Ravix.Documents.Store, otp_app: :ravix +end diff --git a/test/support/test_application.ex b/test/support/test_application.ex index fd46206..d87d562 100644 --- a/test/support/test_application.ex +++ b/test/support/test_application.ex @@ -5,7 +5,8 @@ defmodule Ravix.TestApplication do children = [ {Ravix, [%{}]}, {Ravix.Test.Store, [%{}]}, - {Ravix.Test.NonRetryableStore, [%{}]} + {Ravix.Test.NonRetryableStore, [%{}]}, + {Ravix.Test.OptimisticLockStore, [%{}]} ] Supervisor.init( From c6c8fe898f85c8ab8ccc7b3a0cdc9fef3069d340 Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Sat, 23 Apr 2022 15:45:59 +0200 Subject: [PATCH 12/23] Improving documentation (#5) * Improved queries documentation * Fixing versioning script * Fixing topology update bug --- .github/dependabot.yml | 8 + .github/workflows/hex-deploy.yml | 11 +- README.md | 4 + lib/connection/commands/get_topology.ex | 2 +- lib/connection/connection.ex | 8 +- lib/connection/connection_state_manager.ex | 4 +- lib/connection/executor/request_executor.ex | 12 +- .../executor/request_executor_option.ex | 1 + lib/connection/executor/supervisor.ex | 7 +- lib/connection/models/node_selector.ex | 4 +- lib/connection/supervisor.ex | 4 +- .../commands/data/delete_documents.ex | 1 + lib/documents/commands/data/put_documents.ex | 1 + lib/documents/protocols/create_request.ex | 4 +- lib/documents/protocols/to_json.ex | 2 + lib/documents/session/session.ex | 51 +++- lib/documents/store.ex | 20 ++ lib/operations/database_maintenance.ex | 28 +++ lib/rql/query.ex | 219 +++++++++++++++--- lib/rql/query_parser.ex | 4 +- lib/rql/tokens/and.ex | 1 + lib/rql/tokens/conditions.ex | 102 +++++++- lib/rql/tokens/from.ex | 1 + lib/rql/tokens/group_by.ex | 1 + lib/rql/tokens/limit.ex | 1 + lib/rql/tokens/not.ex | 1 + lib/rql/tokens/or.ex | 1 + lib/rql/tokens/order.ex | 1 + lib/rql/tokens/select.ex | 3 +- lib/rql/tokens/update.ex | 48 ++-- lib/rql/tokens/where.ex | 1 + test/documents/session/session_test.exs | 2 +- test/rql/query_parser_test.exs | 11 +- test/rql/query_test.exs | 3 +- test/support/cat.ex | 1 + test/support/factory.ex | 1 + test/support/non_retryable_store.ex | 1 + test/support/optimistic_lock_store.ex | 1 + test/support/random.ex | 1 + test/support/test_application.ex | 1 + test/support/test_store.ex | 1 + test/support/test_store_invalid.ex | 1 + 42 files changed, 474 insertions(+), 106 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..03be3dc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: mix + directory: "/" + schedule: + interval: daily + time: "02:00" + open-pull-requests-limit: 10 diff --git a/.github/workflows/hex-deploy.yml b/.github/workflows/hex-deploy.yml index 1227ce6..681e966 100644 --- a/.github/workflows/hex-deploy.yml +++ b/.github/workflows/hex-deploy.yml @@ -39,8 +39,15 @@ jobs: run: mix local.hex --force && mix local.rebar --force - name: Install dependencies run: mix deps.get - - name: Set the new version - run: mix version.up + - name: Set the new patch version + if: "contains(github.event.head_commit.message, '[patch]')" + run: mix version.up patch + - name: Set the new minor version + if: "contains(github.event.head_commit.message, '[minor]')" + run: mix version.up minor + - name: Set the new major version + if: "contains(github.event.head_commit.message, '[major]')" + run: mix version.up major - name: Publish to hex.pm run: HEX_API_KEY=${{ secrets.HEX_API_KEY }} mix hex.publish --yes - name: Tag the version diff --git a/README.md b/README.md index e2ada70..e93683c 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,10 @@ config :ravix, Ravix.Test.Store, certificate_file: "/opt/certs/cert.pfx" ``` +## Ecto + +What about querying your RavenDB using Ecto? [Ravix-Ecto](https://github.com/YgorCastor/ravix-ecto) + ## Current State * ~~Configuration Reading~~ diff --git a/lib/connection/commands/get_topology.ex b/lib/connection/commands/get_topology.ex index 0fa1003..6fc95e7 100644 --- a/lib/connection/commands/get_topology.ex +++ b/lib/connection/commands/get_topology.ex @@ -3,7 +3,7 @@ defmodule Ravix.Connection.Commands.GetTopology do Command to fetch the topology data from RavenDB ## Fields - - database_name: The name of the database + - database_name: The name of the database - force_url: Forces the informed url to the new topology """ use Ravix.Documents.Commands.RavenCommand, diff --git a/lib/connection/connection.ex b/lib/connection/connection.ex index 881a3c9..bb65773 100644 --- a/lib/connection/connection.ex +++ b/lib/connection/connection.ex @@ -1,7 +1,5 @@ defmodule Ravix.Connection do - @moduledoc """ - Service to manage the connection with a RavenDB database - """ + @moduledoc false use GenServer require OK @@ -16,7 +14,7 @@ defmodule Ravix.Connection do Receives the reference of a RavenDB store and a initial store state and starts a connection, the connection is registered in the :connections register under the naming StoreModule.Connection - Returns: + Returns: `{:ok, pid}` if the connecion is started `{:error, cause}` if the connection start failed """ @@ -52,7 +50,7 @@ defmodule Ravix.Connection do Triggers a topology update for the specified Ravix.Document.Store, this operation is asynchronous and will be done on background - Returns `:ok` + Returns `:ok` """ @spec update_topology(atom) :: :ok def update_topology(store) do diff --git a/lib/connection/connection_state_manager.ex b/lib/connection/connection_state_manager.ex index 15b55c5..54adc8a 100644 --- a/lib/connection/connection_state_manager.ex +++ b/lib/connection/connection_state_manager.ex @@ -1,7 +1,5 @@ defmodule Ravix.Connection.State.Manager do - @moduledoc """ - Manages the state of a RavenDB Store connection - """ + @moduledoc false require OK require Logger diff --git a/lib/connection/executor/request_executor.ex b/lib/connection/executor/request_executor.ex index 85efd18..31db918 100644 --- a/lib/connection/executor/request_executor.ex +++ b/lib/connection/executor/request_executor.ex @@ -1,11 +1,5 @@ defmodule Ravix.Connection.RequestExecutor do - @moduledoc """ - A process responsible for executing requests to the RavenDB API - - Each RavenDB cluster node has it's own stateful request executor, which - holds how many requests this node executed, the address and infos from the Node and - which requests are being executed in the moment. - """ + @moduledoc false use GenServer use Retry @@ -86,8 +80,8 @@ defmodule Ravix.Connection.RequestExecutor do headers = case conn_state.disable_topology_updates do - false -> headers - true -> [{"Topology-Etag", conn_state.topology_etag}] + true -> headers + false -> [{"Topology-Etag", conn_state.topology_etag}] end execute_for_node(command, node_pid, nil, headers, opts) diff --git a/lib/connection/executor/request_executor_option.ex b/lib/connection/executor/request_executor_option.ex index 5ffee75..30a41ff 100644 --- a/lib/connection/executor/request_executor_option.ex +++ b/lib/connection/executor/request_executor_option.ex @@ -1,4 +1,5 @@ defmodule Ravix.Connection.RequestExecutor.Options do + @moduledoc false alias Ravix.Connection.State, as: ConnectionState def from_connection_state(%ConnectionState{} = conn_state) do diff --git a/lib/connection/executor/supervisor.ex b/lib/connection/executor/supervisor.ex index c542c4b..e14f119 100644 --- a/lib/connection/executor/supervisor.ex +++ b/lib/connection/executor/supervisor.ex @@ -1,10 +1,5 @@ defmodule Ravix.Connection.RequestExecutor.Supervisor do - @moduledoc """ - Supervises the Requests Executors processes - - Each node connection has it own supervised process, so they are completely isolated - from each other. All executors are registered under the :request_executors Registry. - """ + @moduledoc false use DynamicSupervisor require Logger diff --git a/lib/connection/models/node_selector.ex b/lib/connection/models/node_selector.ex index 02b71ba..87c1109 100644 --- a/lib/connection/models/node_selector.ex +++ b/lib/connection/models/node_selector.ex @@ -1,7 +1,5 @@ defmodule Ravix.Connection.NodeSelector do - @moduledoc """ - Strategy to select nodes - """ + @moduledoc false defstruct current_node_index: 0 alias Ravix.Connection.State, as: ConnectionState diff --git a/lib/connection/supervisor.ex b/lib/connection/supervisor.ex index f5c14a6..e2bfe46 100644 --- a/lib/connection/supervisor.ex +++ b/lib/connection/supervisor.ex @@ -1,7 +1,5 @@ defmodule Ravix.Connection.Supervisor do - @moduledoc """ - Supervises and triggers the initialization of a Raven Store - """ + @moduledoc false use Supervisor alias Ravix.Connection diff --git a/lib/documents/commands/data/delete_documents.ex b/lib/documents/commands/data/delete_documents.ex index a1b9b3a..f5e2365 100644 --- a/lib/documents/commands/data/delete_documents.ex +++ b/lib/documents/commands/data/delete_documents.ex @@ -1,4 +1,5 @@ defmodule Ravix.Documents.Commands.Data.DeleteDocument do + @moduledoc false @derive {Jason.Encoder, only: [:Id, :ChangeVector, :Type]} defstruct Id: nil, ChangeVector: nil, diff --git a/lib/documents/commands/data/put_documents.ex b/lib/documents/commands/data/put_documents.ex index 79cba02..eaf7f7b 100644 --- a/lib/documents/commands/data/put_documents.ex +++ b/lib/documents/commands/data/put_documents.ex @@ -1,4 +1,5 @@ defmodule Ravix.Documents.Commands.Data.PutDocument do + @moduledoc false @derive {Jason.Encoder, only: [:Id, :Document, :ChangeVector, :Type]} defstruct Id: nil, Document: nil, diff --git a/lib/documents/protocols/create_request.ex b/lib/documents/protocols/create_request.ex index 4de0072..3acf077 100644 --- a/lib/documents/protocols/create_request.ex +++ b/lib/documents/protocols/create_request.ex @@ -1,7 +1,5 @@ defprotocol Ravix.Documents.Protocols.CreateRequest do - @moduledoc """ - Protocol to define how commands are converted to requests - """ + @moduledoc false @doc """ Creates a request based on the command diff --git a/lib/documents/protocols/to_json.ex b/lib/documents/protocols/to_json.ex index 641d840..477633d 100644 --- a/lib/documents/protocols/to_json.ex +++ b/lib/documents/protocols/to_json.ex @@ -1,4 +1,6 @@ defprotocol Ravix.Documents.Protocols.ToJson do + @moduledoc false + @spec to_json(t) :: any() @fallback_to_any true def to_json(command) diff --git a/lib/documents/session/session.ex b/lib/documents/session/session.ex index 52a40e1..fe7a136 100644 --- a/lib/documents/session/session.ex +++ b/lib/documents/session/session.ex @@ -1,6 +1,6 @@ defmodule Ravix.Documents.Session do @moduledoc """ - A stateful session to execute ravendb commands + A stateful session to execute RavenDB commands """ use GenServer @@ -36,6 +36,25 @@ defmodule Ravix.Documents.Session do ## Returns - `{:ok, results}` - `{:errors, cause}` + + ## Examples + iex> Session.load(session_id, "entity_id") + {:ok, + %{ + "Includes" => %{}, + "Results" => [ + %{ + "@metadata" => %{ + "@change-vector" => "A:6450-HJrwf2z3c0G/FHJPm3zK3w", + "@id" => "f13ffb17-ed7d-43b6-a483-23993db70958", + "@last-modified" => "2022-04-23T11:14:16.4277047Z" + }, + "cat_name" => "Coco", + "id" => "f13ffb17-ed7d-43b6-a483-23993db70958" + } + ], + "already_loaded_ids" => [] + }} """ @spec load(binary(), list() | bitstring(), any, keyword() | nil) :: any def load(session_id, ids, includes \\ nil, opts \\ nil) @@ -58,13 +77,13 @@ defmodule Ravix.Documents.Session do ## Parameters - session_id: the session id - - entity: the document to be deleted + - entity/entity_id: the document to be deleted ## Returns - - `{:ok, updated_state}` + - `{:ok, Ravix.Documents.Session.State}` - `{:error, cause}` """ - @spec delete(binary, map()) :: any + @spec delete(binary, map() | binary()) :: any def delete(session_id, entity) when is_map_key(entity, :id) do delete(session_id, entity.id) end @@ -85,7 +104,7 @@ defmodule Ravix.Documents.Session do - change_vector: the concurrency change vector ## Returns - - `{:ok, updated_session}` + - `{:ok, Ravix.Documents.Session.State}` - `{:error, cause}` """ @spec store(binary(), map(), binary() | nil, binary() | nil) :: any @@ -102,6 +121,22 @@ defmodule Ravix.Documents.Session do @doc """ Persists the session changes to the RavenDB database + + Returns a [RavenDB batch response](https://ravendb.net/docs/article-page/5.3/csharp/client-api/rest-api/document-commands/batch-commands#response-format) + + ## Examples + iex> Session.save_changes(session_id) + {:ok, + %{ + "Results" => [ + %{ + "ChangeVector" => nil, + "Deleted" => true, + "Id" => "3421125e-416a-4bce-bb56-56cb4a7991ae", + "Type" => "DELETE" + } + ] + }} """ @spec save_changes(binary) :: any def save_changes(session_id) do @@ -112,6 +147,8 @@ defmodule Ravix.Documents.Session do @doc """ Fetches the current session state + + Returns a `{:ok, Ravix.Documents.Session.State}` """ @spec fetch_state(binary()) :: {:error, :session_not_found} | {:ok, SessionState.t()} def fetch_state(session_id) do @@ -132,6 +169,8 @@ defmodule Ravix.Documents.Session do - query: The `Ravix.RQL.Query` to be executed - session_id: the session_id - method: The http method + + Returns a RavenDB query response """ @spec execute_query(any, binary, any) :: any def execute_query(query, session_id, method) do @@ -204,7 +243,7 @@ defmodule Ravix.Documents.Session do def handle_call({:delete, id}, _from, %SessionState{} = state) do case SessionManager.delete_document(state, id) do - {:ok, updated_state} -> {:reply, {:ok, id}, updated_state} + {:ok, updated_state} -> {:reply, {:ok, updated_state}, updated_state} {:error, err} -> {:reply, {:error, err}, state} end end diff --git a/lib/documents/store.ex b/lib/documents/store.ex index 3668b43..7543644 100644 --- a/lib/documents/store.ex +++ b/lib/documents/store.ex @@ -58,7 +58,27 @@ defmodule Ravix.Documents.Store do | {:error, {:already_started, pid}} | {:error, term} + @doc """ + Opens a RavenDB local session + + Returns a tuple with `{:ok, uuid}` if successful or `{:error, :not_found}` if the store + is not initialized + + ## Examples + iex> Ravix.Test.Store.open_session + {:ok, "8945c215-dd67-44da-9a64-2916e0a328d9"} + """ @callback open_session() :: {:ok, binary()} + @doc """ + Closes a RavenDB local session + + Returns `:ok` if successful or `{:error, :not_found}` if the session + is not found + + ## Examples + iex> Ravix.Test.Store.close_session("8945c215-dd67-44da-9a64-2916e0a328d9") + :ok + """ @callback close_session(session_id :: binary()) :: :ok | {:error, :not_found} end diff --git a/lib/operations/database_maintenance.ex b/lib/operations/database_maintenance.ex index 01c9b3c..0e4e31d 100644 --- a/lib/operations/database_maintenance.ex +++ b/lib/operations/database_maintenance.ex @@ -9,6 +9,34 @@ defmodule Ravix.Operations.Database.Maintenance do @doc """ Creates a database using the informed request executor + + Options: + - :encrypted = true/false + - :disabled = true/false + - :replication_factor = 1-N + + ## Examples + iex> Ravix.Operations.Database.Maintenance.create_database(Ravix.Test.Store, "test_db") + {:ok, + %{ + "Name" => "test_db", + "NodesAddedTo" => ["http://4e0373cbf5d0:8080"], + "RaftCommandIndex" => 443, + "Topology" => %{ + "ClusterTransactionIdBase64" => "mdO7gPZsMEeslGOxxNfpjA", + "DatabaseTopologyIdBase64" => "0FHV8Uc0jEi94uZQiT00mA", + "DemotionReasons" => %{}, + "DynamicNodesDistribution" => false, + "Members" => ["A"], + "NodesModifiedAt" => "2022-04-23T11:00:06.9470373Z", + "PriorityOrder" => [], + "Promotables" => [], + "PromotablesStatus" => %{}, + "Rehabs" => [], + "ReplicationFactor" => 1, + "Stamp" => %{"Index" => 443, "LeadersTicks" => -2, "Term" => 4} + } + }} """ @spec create_database(atom | pid, nil | binary, keyword) :: {:error, any} | {:ok, any} def create_database(store_or_pid, database_name, opts \\ []) diff --git a/lib/rql/query.ex b/lib/rql/query.ex index 7460189..620de7d 100644 --- a/lib/rql/query.ex +++ b/lib/rql/query.ex @@ -56,7 +56,12 @@ defmodule Ravix.RQL.Query do } @doc """ - Adds a `Ravix.RQL.Tokens.From` to the query + 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} @@ -68,9 +73,18 @@ defmodule Ravix.RQL.Query do end @doc """ - Adds a `Ravix.RQL.Tokens.From` to the query with an alias + 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") """ - def from(document, as_alias) do + @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) @@ -78,16 +92,20 @@ defmodule Ravix.RQL.Query do end @doc """ - Adds a `Ravix.RQL.Tokens.Update` to the query - """ - @spec update(Query.t(), list(map()) | Update.t()) :: Query.t() - def update(%Query{} = query, document_updates) when is_list(document_updates) do - %Query{ - query - | update_token: Update.fields(document_updates) - } - end + 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 @@ -96,7 +114,13 @@ defmodule Ravix.RQL.Query do end @doc """ - Adds a `Ravix.RQL.Tokens.Where` to the query + 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 @@ -107,7 +131,13 @@ defmodule Ravix.RQL.Query do end @doc """ - Adds a `Ravix.RQL.Tokens.Select` to the query + 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 @@ -117,6 +147,15 @@ defmodule Ravix.RQL.Query do } 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{ @@ -126,7 +165,14 @@ defmodule Ravix.RQL.Query do end @doc """ - Adds a `Ravix.RQL.Tokens.And` to the query + 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 @@ -136,6 +182,16 @@ defmodule Ravix.RQL.Query do } 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{ @@ -144,6 +200,34 @@ defmodule Ravix.RQL.Query do } 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{ @@ -152,6 +236,15 @@ defmodule Ravix.RQL.Query do } 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{ @@ -161,16 +254,14 @@ defmodule Ravix.RQL.Query do end @doc """ - Adds a `Ravix.RQL.Tokens.Or` to the query - """ - @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 + 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{ @@ -179,6 +270,15 @@ defmodule Ravix.RQL.Query do } 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()}, ...] @@ -194,6 +294,11 @@ defmodule Ravix.RQL.Query do @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 @@ -204,7 +309,12 @@ defmodule Ravix.RQL.Query do end @doc """ - Create a Query using a raw RQL string with parameters + 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 @@ -216,7 +326,43 @@ defmodule Ravix.RQL.Query do end @doc """ - Executes a list query in the informed session + 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 @@ -224,7 +370,15 @@ defmodule Ravix.RQL.Query do end @doc """ - Delete all the documents that matches the informed query + 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 @@ -232,7 +386,16 @@ defmodule Ravix.RQL.Query do end @doc """ - Updates all the documents that matches the informed query + 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 diff --git a/lib/rql/query_parser.ex b/lib/rql/query_parser.ex index 7a3ff0f..5ffbd7c 100644 --- a/lib/rql/query_parser.ex +++ b/lib/rql/query_parser.ex @@ -1,7 +1,5 @@ defmodule Ravix.RQL.QueryParser do - @moduledoc """ - Parsing tokens to RQL - """ + @moduledoc false require OK alias Ravix.RQL.Query diff --git a/lib/rql/tokens/and.ex b/lib/rql/tokens/and.ex index 0742e46..f491bcf 100644 --- a/lib/rql/tokens/and.ex +++ b/lib/rql/tokens/and.ex @@ -1,4 +1,5 @@ defmodule Ravix.RQL.Tokens.And do + @moduledoc false defstruct [ :token, :condition diff --git a/lib/rql/tokens/conditions.ex b/lib/rql/tokens/conditions.ex index e390808..6506b09 100644 --- a/lib/rql/tokens/conditions.ex +++ b/lib/rql/tokens/conditions.ex @@ -1,4 +1,7 @@ defmodule Ravix.RQL.Tokens.Condition do + @moduledoc """ + Supported RQL Conditions + """ defstruct [ :token, :field, @@ -13,7 +16,16 @@ defmodule Ravix.RQL.Tokens.Condition do params: list() } - @spec greater_than(String.t(), any) :: Condition.t() + @doc """ + Greater than condition + + Returns a `Ravix.RQL.Tokens.Condition` + + ## Examples + iex> import Ravix.RQL.Tokens.Condition + iex> greater_than("field", 10) + """ + @spec greater_than(atom() | String.t(), number()) :: Condition.t() def greater_than(field_name, value) do %Condition{ token: :greater_than, @@ -22,7 +34,16 @@ defmodule Ravix.RQL.Tokens.Condition do } end - @spec greater_than_or_equal_to(String.t(), any) :: Condition.t() + @doc """ + Greater than or equal to condition + + Returns a `Ravix.RQL.Tokens.Condition` + + ## Examples + iex> import Ravix.RQL.Tokens.Condition + iex> greater_than_or_equal_to("field", 10) + """ + @spec greater_than_or_equal_to(atom() | String.t(), number()) :: Condition.t() def greater_than_or_equal_to(field_name, value) do %Condition{ token: :greater_than_or_eq, @@ -31,7 +52,16 @@ defmodule Ravix.RQL.Tokens.Condition do } end - @spec lower_than(String.t(), any) :: Condition.t() + @doc """ + Lower than condition + + Returns a `Ravix.RQL.Tokens.Condition` + + ## Examples + iex> import Ravix.RQL.Tokens.Condition + iex> lower_than("field", 10) + """ + @spec lower_than(atom() | String.t(), number()) :: Condition.t() def lower_than(field_name, value) do %Condition{ token: :lower_than, @@ -40,7 +70,16 @@ defmodule Ravix.RQL.Tokens.Condition do } end - @spec lower_than_or_equal_to(String.t(), any) :: Condition.t() + @doc """ + Lower than or equal to condition + + Returns a `Ravix.RQL.Tokens.Condition` + + ## Examples + iex> import Ravix.RQL.Tokens.Condition + iex> lower_than_or_equal_to("field", 10) + """ + @spec lower_than_or_equal_to(atom() | String.t(), number()) :: Condition.t() def lower_than_or_equal_to(field_name, value) do %Condition{ token: :lower_than_or_eq, @@ -49,7 +88,16 @@ defmodule Ravix.RQL.Tokens.Condition do } end - @spec equal_to(String.t(), any) :: Condition.t() + @doc """ + Equal to condition + + Returns a `Ravix.RQL.Tokens.Condition` + + ## Examples + iex> import Ravix.RQL.Tokens.Condition + iex> equal_to("field", "value") + """ + @spec equal_to(atom() | String.t(), any) :: Condition.t() def equal_to(field_name, value) do %Condition{ token: :eq, @@ -58,7 +106,16 @@ defmodule Ravix.RQL.Tokens.Condition do } end - @spec not_equal_to(String.t(), any) :: Condition.t() + @doc """ + Not Equal to condition + + Returns a `Ravix.RQL.Tokens.Condition` + + ## Examples + iex> import Ravix.RQL.Tokens.Condition + iex> not_equal_to("field", "value") + """ + @spec not_equal_to(atom() | String.t(), any) :: Condition.t() def not_equal_to(field_name, value) do %Condition{ token: :ne, @@ -67,7 +124,16 @@ defmodule Ravix.RQL.Tokens.Condition do } end - @spec in?(String.t(), any) :: Condition.t() + @doc """ + Specifies that the value is in a list + + Returns a `Ravix.RQL.Tokens.Condition` + + ## Examples + iex> import Ravix.RQL.Tokens.Condition + iex> in?("field", [1,2,3]) + """ + @spec in?(atom() | String.t(), list()) :: Condition.t() def in?(field_name, values) do %Condition{ token: :in, @@ -76,7 +142,16 @@ defmodule Ravix.RQL.Tokens.Condition do } end - @spec not_in(String.t(), any) :: Condition.t() + @doc """ + Specifies that the value is not in a list + + Returns a `Ravix.RQL.Tokens.Condition` + + ## Examples + iex> import Ravix.RQL.Tokens.Condition + iex> not_in("field", ["a", "b", "c"]) + """ + @spec not_in(atom() | String.t(), list()) :: Condition.t() def not_in(field_name, values) do %Condition{ token: :nin, @@ -85,7 +160,16 @@ defmodule Ravix.RQL.Tokens.Condition do } end - @spec between(String.t(), any) :: Condition.t() + @doc """ + Specifies that the value is between two values + + Returns a `Ravix.RQL.Tokens.Condition` + + ## Examples + iex> import Ravix.RQL.Tokens.Condition + iex> between("field", [15,25]) + """ + @spec between(atom() | String.t(), list()) :: Condition.t() def between(field_name, values) do %Condition{ token: :between, diff --git a/lib/rql/tokens/from.ex b/lib/rql/tokens/from.ex index 466860a..8df9ffb 100644 --- a/lib/rql/tokens/from.ex +++ b/lib/rql/tokens/from.ex @@ -1,4 +1,5 @@ defmodule Ravix.RQL.Tokens.From do + @moduledoc false defstruct [ :token, :as_alias, diff --git a/lib/rql/tokens/group_by.ex b/lib/rql/tokens/group_by.ex index 8ba0ff8..c1faf07 100644 --- a/lib/rql/tokens/group_by.ex +++ b/lib/rql/tokens/group_by.ex @@ -1,4 +1,5 @@ defmodule Ravix.RQL.Tokens.Group do + @moduledoc false defstruct [ :token, :fields diff --git a/lib/rql/tokens/limit.ex b/lib/rql/tokens/limit.ex index ba70812..0e7e6af 100644 --- a/lib/rql/tokens/limit.ex +++ b/lib/rql/tokens/limit.ex @@ -1,4 +1,5 @@ defmodule Ravix.RQL.Tokens.Limit do + @moduledoc false defstruct [ :token, :skip, diff --git a/lib/rql/tokens/not.ex b/lib/rql/tokens/not.ex index 2d8dfb0..955a202 100644 --- a/lib/rql/tokens/not.ex +++ b/lib/rql/tokens/not.ex @@ -1,4 +1,5 @@ defmodule Ravix.RQL.Tokens.Not do + @moduledoc false defstruct [ :token, :condition diff --git a/lib/rql/tokens/or.ex b/lib/rql/tokens/or.ex index 4999af2..5442ba5 100644 --- a/lib/rql/tokens/or.ex +++ b/lib/rql/tokens/or.ex @@ -1,4 +1,5 @@ defmodule Ravix.RQL.Tokens.Or do + @moduledoc false defstruct [ :token, :condition diff --git a/lib/rql/tokens/order.ex b/lib/rql/tokens/order.ex index c7a91a5..20d05cd 100644 --- a/lib/rql/tokens/order.ex +++ b/lib/rql/tokens/order.ex @@ -1,4 +1,5 @@ defmodule Ravix.RQL.Tokens.Order do + @moduledoc false defstruct [ :token, :fields diff --git a/lib/rql/tokens/select.ex b/lib/rql/tokens/select.ex index 4a5c0e3..cd7e6d2 100644 --- a/lib/rql/tokens/select.ex +++ b/lib/rql/tokens/select.ex @@ -1,4 +1,5 @@ defmodule Ravix.RQL.Tokens.Select do + @moduledoc false defstruct [ :token, :fields @@ -6,7 +7,7 @@ defmodule Ravix.RQL.Tokens.Select do alias Ravix.RQL.Tokens.Select - @type field_name :: String.t() + @type field_name :: atom() | String.t() @type field_alias :: String.t() @type allowed_select_params :: list(field_name() | {field_name(), field_alias()}) diff --git a/lib/rql/tokens/update.ex b/lib/rql/tokens/update.ex index 236c738..037a2e9 100644 --- a/lib/rql/tokens/update.ex +++ b/lib/rql/tokens/update.ex @@ -1,4 +1,7 @@ defmodule Ravix.RQL.Tokens.Update do + @moduledoc """ + RQL Update tokens + """ defstruct token: :update, fields: [] @@ -9,32 +12,51 @@ defmodule Ravix.RQL.Tokens.Update do fields: list(map()) } - @spec fields(list(map())) :: Update.t() - def fields(fields) do - %Update{ - token: :update, - fields: fields - } - end + @doc """ + Creates a new "set" update operation + + Returns a `Ravix.RQL.Tokens.Update` - @spec set(Update.t(), String.t(), any) :: Ravix.RQL.Tokens.Update.t() - def set(update, field, value) do + ## Examples + iex> import alias Ravix.RQL.Tokens.Update + iex> set("field1", 10) |> set("field2", "a") + """ + @spec set(Update.t(), atom() | String.t(), any) :: Ravix.RQL.Tokens.Update.t() + def set(update \\ %Update{}, field, value) do %Update{ update | fields: update.fields ++ [%{name: field, value: value, operation: :set}] } end - @spec inc(Update.t(), String.t(), any) :: Ravix.RQL.Tokens.Update.t() - def inc(update, field, value) do + @doc """ + Creates a new "increment" update operation + + Returns a `Ravix.RQL.Tokens.Update` + + ## Examples + iex> import alias Ravix.RQL.Tokens.Update + iex> inc("field1", 10) + """ + @spec inc(Update.t(), atom() | String.t(), number()) :: Ravix.RQL.Tokens.Update.t() + def inc(update \\ %Update{}, field, value) when is_number(value) do %Update{ update | fields: update.fields ++ [%{name: field, value: value, operation: :inc}] } end - @spec dec(Update.t(), String.t(), any) :: Ravix.RQL.Tokens.Update.t() - def dec(update, field, value) do + @doc """ + Creates a new "decrement" update operation + + Returns a `Ravix.RQL.Tokens.Update` + + ## Examples + iex> import alias Ravix.RQL.Tokens.Update + iex> dec("field1", 10) + """ + @spec dec(Update.t(), atom() | String.t(), number()) :: Ravix.RQL.Tokens.Update.t() + def dec(update \\ %Update{}, field, value) when is_number(value) do %Update{ update | fields: update.fields ++ [%{name: field, value: value, operation: :dec}] diff --git a/lib/rql/tokens/where.ex b/lib/rql/tokens/where.ex index ceafe0e..e5b6ceb 100644 --- a/lib/rql/tokens/where.ex +++ b/lib/rql/tokens/where.ex @@ -1,4 +1,5 @@ defmodule Ravix.RQL.Tokens.Where do + @moduledoc false defstruct [ :token, :condition diff --git a/test/documents/session/session_test.exs b/test/documents/session/session_test.exs index 466e513..a921137 100644 --- a/test/documents/session/session_test.exs +++ b/test/documents/session/session_test.exs @@ -375,7 +375,7 @@ defmodule Ravix.Documents.SessionTest do response = result[:response] state = result[:state] - assert response == any_entity.id + assert length(response.deleted_entities) == 1 assert state.documents_by_id == %{} end diff --git a/test/rql/query_parser_test.exs b/test/rql/query_parser_test.exs index 303b6e1..804b61f 100644 --- a/test/rql/query_parser_test.exs +++ b/test/rql/query_parser_test.exs @@ -3,11 +3,10 @@ defmodule Ravix.RQL.QueryParserTest do import Ravix.RQL.Query import Ravix.RQL.Tokens.Condition + import Ravix.RQL.Tokens.Update alias Ravix.RQL.QueryParser alias Ravix.RQL.Tokens.Condition - alias Ravix.RQL.Tokens.Update - describe "parse/1" do test "It should parse the tokens succesfully" do @@ -43,17 +42,13 @@ defmodule Ravix.RQL.QueryParserTest do end test "Should parse an update succesfully" do - updates = [ - %{name: "field", value: "new_value", operation: :set}, - %{name: "field2", value: 1, operation: :inc}, - %{name: "field3", value: 2, operation: :dec} - ] + updates = set("field", "new_value") |> inc("field2", 1) |> dec("field3", 2) {:ok, query_result} = from("test", "t") |> where(greater_than("field", 10)) |> and?(equal_to("field2", "asdf")) - |> update(Update.fields(updates)) + |> update(updates) |> QueryParser.parse() assert query_result.query_string == diff --git a/test/rql/query_test.exs b/test/rql/query_test.exs index 47afd3c..6d8bc7f 100644 --- a/test/rql/query_test.exs +++ b/test/rql/query_test.exs @@ -8,7 +8,6 @@ defmodule Ravix.RQL.QueryTest do import Ravix.Factory alias Ravix.Documents.Session - alias Ravix.RQL.Tokens.Update alias Ravix.Test.Store, as: Store alias Ravix.Test.NonRetryableStore @@ -464,7 +463,7 @@ defmodule Ravix.RQL.QueryTest do update_response <- from("@all_docs", "a") - |> update(set(%Update{}, :cat_name, "Fluffer, the hand-ripper")) + |> update(set(:cat_name, "Fluffer, the hand-ripper")) |> where(equal_to("cat_name", any_entity.cat_name)) |> update_for(session_id) diff --git a/test/support/cat.ex b/test/support/cat.ex index 4679bab..70f3ebd 100644 --- a/test/support/cat.ex +++ b/test/support/cat.ex @@ -1,3 +1,4 @@ defmodule Ravix.SampleModel.Cat do + @moduledoc false use Ravix.Document, fields: [:id, :name, :breed] end diff --git a/test/support/factory.ex b/test/support/factory.ex index 28c5503..b05e1c0 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -1,4 +1,5 @@ defmodule Ravix.Factory do + @moduledoc false use ExMachina alias Ravix.Documents.Session diff --git a/test/support/non_retryable_store.ex b/test/support/non_retryable_store.ex index c57c80b..2ecacba 100644 --- a/test/support/non_retryable_store.ex +++ b/test/support/non_retryable_store.ex @@ -1,3 +1,4 @@ defmodule Ravix.Test.NonRetryableStore do + @moduledoc false use Ravix.Documents.Store, otp_app: :ravix end diff --git a/test/support/optimistic_lock_store.ex b/test/support/optimistic_lock_store.ex index a279538..2b83b7c 100644 --- a/test/support/optimistic_lock_store.ex +++ b/test/support/optimistic_lock_store.ex @@ -1,3 +1,4 @@ defmodule Ravix.Test.OptimisticLockStore do + @moduledoc false use Ravix.Documents.Store, otp_app: :ravix end diff --git a/test/support/random.ex b/test/support/random.ex index c0b33eb..18ec7ca 100644 --- a/test/support/random.ex +++ b/test/support/random.ex @@ -1,4 +1,5 @@ defmodule Ravix.Test.Random do + @moduledoc false def safe_random_string(size) do :crypto.strong_rand_bytes(size) |> Base.url_encode64() |> binary_part(0, size) end diff --git a/test/support/test_application.ex b/test/support/test_application.ex index d87d562..4c11754 100644 --- a/test/support/test_application.ex +++ b/test/support/test_application.ex @@ -1,4 +1,5 @@ defmodule Ravix.TestApplication do + @moduledoc false use Supervisor def init(_opts) do diff --git a/test/support/test_store.ex b/test/support/test_store.ex index 4151fe8..92a8fb4 100644 --- a/test/support/test_store.ex +++ b/test/support/test_store.ex @@ -1,3 +1,4 @@ defmodule Ravix.Test.Store do + @moduledoc false use Ravix.Documents.Store, otp_app: :ravix end diff --git a/test/support/test_store_invalid.ex b/test/support/test_store_invalid.ex index f993ea3..2c56a71 100644 --- a/test/support/test_store_invalid.ex +++ b/test/support/test_store_invalid.ex @@ -1,3 +1,4 @@ defmodule Ravix.TestStoreInvalid do + @moduledoc false use Ravix.Documents.Store, otp_app: :ravix end From 4d6813532dcdf4b29bcd979e44bdf1954aa5147b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 23 Apr 2022 13:52:54 +0000 Subject: [PATCH 13/23] v0.1.0 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 648744a..9b4f491 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Ravix.MixProject do def project do [ app: :ravix, - version: "0.0.3", + version: "0.1.0", elixir: "~> 1.13", start_permanent: Mix.env() == :prod, deps: deps(), From a1431c504d1e5a6cc307cc71d56484221f2c0a07 Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Tue, 26 Apr 2022 20:50:34 +0200 Subject: [PATCH 14/23] Adding database delete and status commands --- .../commands/delete_database_command.ex | 34 +++++++++++ .../commands/get_statistics_command.ex | 29 +++++++++ lib/operations/database_maintenance.ex | 61 ++++++++++++++++++- test/operations/database_maintenance_test.exs | 15 ++++- 4 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 lib/operations/commands/delete_database_command.ex create mode 100644 lib/operations/commands/get_statistics_command.ex diff --git a/lib/operations/commands/delete_database_command.ex b/lib/operations/commands/delete_database_command.ex new file mode 100644 index 0000000..1040217 --- /dev/null +++ b/lib/operations/commands/delete_database_command.ex @@ -0,0 +1,34 @@ +defmodule Ravix.Operations.Database.Commands.DeleteDatabaseCommand do + @moduledoc """ + Command to delete a RavenDB database + """ + @derive {Jason.Encoder, only: [:DatabaseNames, :HardDelete, :TimeToWaitForConfirmation]} + use Ravix.Documents.Commands.RavenCommand, + DatabaseNames: [], + HardDelete: false, + TimeToWaitForConfirmation: nil + + import Ravix.Documents.Commands.RavenCommand + + alias Ravix.Operations.Database.Commands.DeleteDatabaseCommand + alias Ravix.Documents.Protocols.{ToJson, CreateRequest} + alias Ravix.Connection.ServerNode + + command_type(%{ + DatabaseNames: list(String.t()), + HardDelete: boolean(), + TimeToWaitForConfirmation: non_neg_integer() + }) + + defimpl CreateRequest, for: DeleteDatabaseCommand do + @spec create_request(DeleteDatabaseCommand.t(), ServerNode.t()) :: DeleteDatabaseCommand.t() + def create_request(%DeleteDatabaseCommand{} = command, %ServerNode{} = _server_node) do + %DeleteDatabaseCommand{ + command + | url: "/admin/databases", + method: "DELETE", + data: command |> ToJson.to_json() + } + end + end +end diff --git a/lib/operations/commands/get_statistics_command.ex b/lib/operations/commands/get_statistics_command.ex new file mode 100644 index 0000000..cc91341 --- /dev/null +++ b/lib/operations/commands/get_statistics_command.ex @@ -0,0 +1,29 @@ +defmodule Ravix.Operations.Database.Commands.GetStatisticsCommand do + @moduledoc """ + Command to delete a RavenDB database + """ + @derive {Jason.Encoder, only: [:debugTag]} + use Ravix.Documents.Commands.RavenCommand, + debugTag: nil + + import Ravix.Documents.Commands.RavenCommand + + alias Ravix.Operations.Database.Commands.GetStatisticsCommand + alias Ravix.Documents.Protocols.CreateRequest + alias Ravix.Connection.ServerNode + + command_type(%{ + debugTag: String.t() + }) + + defimpl CreateRequest, for: GetStatisticsCommand do + @spec create_request(GetStatisticsCommand.t(), ServerNode.t()) :: GetStatisticsCommand.t() + def create_request(%GetStatisticsCommand{} = command, %ServerNode{} = server_node) do + %GetStatisticsCommand{ + command + | url: "/databases/#{server_node.database}/stats?" <> command.debugTag, + method: "GET" + } + end + end +end diff --git a/lib/operations/database_maintenance.ex b/lib/operations/database_maintenance.ex index 0e4e31d..be08f90 100644 --- a/lib/operations/database_maintenance.ex +++ b/lib/operations/database_maintenance.ex @@ -4,7 +4,12 @@ defmodule Ravix.Operations.Database.Maintenance do """ require OK - alias Ravix.Operations.Database.Commands.CreateDatabaseCommand + alias Ravix.Operations.Database.Commands.{ + CreateDatabaseCommand, + DeleteDatabaseCommand, + GetStatisticsCommand + } + alias Ravix.Connection.RequestExecutor @doc """ @@ -66,4 +71,58 @@ defmodule Ravix.Operations.Database.Maintenance do response.data end end + + @spec delete_database(atom | pid, nil | binary, keyword) :: {:error, any} | {:ok, any} + def delete_database(store_or_pid, database_name, opts \\ []) + + def delete_database(store, database_name, opts) when is_atom(store) do + node_pid = RequestExecutor.Supervisor.fetch_nodes(store) |> Enum.at(0) + + delete_database(node_pid, database_name, opts) + end + + def delete_database(node_pid, database_name, opts) when is_pid(node_pid) do + OK.for do + response <- + %DeleteDatabaseCommand{ + DatabaseNames: [database_name], + HardDelete: Keyword.get(opts, :hard_delete, false), + TimeToWaitForConfirmation: Keyword.get(opts, :time_for_confirmation, 100) + } + |> RequestExecutor.execute_for_node( + node_pid, + database_name, + {}, + opts + ) + after + response.data + end + end + + @spec database_stats(atom | pid, nil | binary, keyword) :: {:error, any} | {:ok, any} + def database_stats(store_or_pid, database_name, opts \\ []) + + def database_stats(store, database_name, opts) when is_atom(store) do + node_pid = RequestExecutor.Supervisor.fetch_nodes(store) |> Enum.at(0) + + database_stats(node_pid, database_name, opts) + end + + def database_stats(node_pid, database_name, opts) when is_pid(node_pid) do + OK.for do + response <- + %GetStatisticsCommand{ + debugTag: Keyword.get(opts, :debug_tag, "") + } + |> RequestExecutor.execute_for_node( + node_pid, + database_name, + {}, + opts + ) + after + response.data + end + end end diff --git a/test/operations/database_maintenance_test.exs b/test/operations/database_maintenance_test.exs index d22a5b1..259b907 100644 --- a/test/operations/database_maintenance_test.exs +++ b/test/operations/database_maintenance_test.exs @@ -9,13 +9,15 @@ defmodule Ravix.Operations.Database.MaintenanceTest do :ok end - describe "create_database/0" do + describe "create_database/0 and delete_database" do test "should create a new database successfully" do db_name = Ravix.Test.Random.safe_random_string(5) {:ok, created} = Maintenance.create_database(NonRetryableStore, db_name) assert created["Name"] == db_name + + {:ok, %{"PendingDeletes" => []}} = Maintenance.delete_database(NonRetryableStore, db_name) end test "If the database already exists, should return an error" do @@ -27,4 +29,15 @@ defmodule Ravix.Operations.Database.MaintenanceTest do assert err == "Database '#{db_name}' already exists!" end end + + describe "database_stats/2" do + test "Should fetch database stats if it exists" do + db_name = Ravix.Test.Random.safe_random_string(5) + _ = Maintenance.create_database(NonRetryableStore, db_name) + + {:ok, %{"LastDatabaseEtag" => 0}} = Maintenance.database_stats(NonRetryableStore, db_name) + + _ = Maintenance.delete_database(NonRetryableStore, db_name) + end + end end From dc2d280827683619c6c208bb3132ab70881b62db Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Wed, 27 Apr 2022 08:19:11 +0200 Subject: [PATCH 15/23] Allowing the statistics command to fetch data from other databases --- lib/operations/commands/get_statistics_command.ex | 14 +++++++++++--- lib/operations/database_maintenance.ex | 5 +++-- test/operations/database_maintenance_test.exs | 9 ++------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/operations/commands/get_statistics_command.ex b/lib/operations/commands/get_statistics_command.ex index cc91341..1a4a676 100644 --- a/lib/operations/commands/get_statistics_command.ex +++ b/lib/operations/commands/get_statistics_command.ex @@ -4,7 +4,8 @@ defmodule Ravix.Operations.Database.Commands.GetStatisticsCommand do """ @derive {Jason.Encoder, only: [:debugTag]} use Ravix.Documents.Commands.RavenCommand, - debugTag: nil + debugTag: nil, + databaseName: nil import Ravix.Documents.Commands.RavenCommand @@ -13,15 +14,22 @@ defmodule Ravix.Operations.Database.Commands.GetStatisticsCommand do alias Ravix.Connection.ServerNode command_type(%{ - debugTag: String.t() + debugTag: String.t(), + databaseName: String.t() | nil }) defimpl CreateRequest, for: GetStatisticsCommand do @spec create_request(GetStatisticsCommand.t(), ServerNode.t()) :: GetStatisticsCommand.t() def create_request(%GetStatisticsCommand{} = command, %ServerNode{} = server_node) do + database_name = + case command.databaseName do + nil -> server_node.database + database_name -> database_name + end + %GetStatisticsCommand{ command - | url: "/databases/#{server_node.database}/stats?" <> command.debugTag, + | url: "/databases/#{database_name}/stats?" <> command.debugTag, method: "GET" } end diff --git a/lib/operations/database_maintenance.ex b/lib/operations/database_maintenance.ex index be08f90..e9203ba 100644 --- a/lib/operations/database_maintenance.ex +++ b/lib/operations/database_maintenance.ex @@ -101,7 +101,7 @@ defmodule Ravix.Operations.Database.Maintenance do end @spec database_stats(atom | pid, nil | binary, keyword) :: {:error, any} | {:ok, any} - def database_stats(store_or_pid, database_name, opts \\ []) + def database_stats(store_or_pid, database_name \\ nil, opts \\ []) def database_stats(store, database_name, opts) when is_atom(store) do node_pid = RequestExecutor.Supervisor.fetch_nodes(store) |> Enum.at(0) @@ -113,7 +113,8 @@ defmodule Ravix.Operations.Database.Maintenance do OK.for do response <- %GetStatisticsCommand{ - debugTag: Keyword.get(opts, :debug_tag, "") + debugTag: Keyword.get(opts, :debug_tag, ""), + databaseName: database_name } |> RequestExecutor.execute_for_node( node_pid, diff --git a/test/operations/database_maintenance_test.exs b/test/operations/database_maintenance_test.exs index 259b907..7f9c48f 100644 --- a/test/operations/database_maintenance_test.exs +++ b/test/operations/database_maintenance_test.exs @@ -30,14 +30,9 @@ defmodule Ravix.Operations.Database.MaintenanceTest do end end - describe "database_stats/2" do + describe "database_stats/1" do test "Should fetch database stats if it exists" do - db_name = Ravix.Test.Random.safe_random_string(5) - _ = Maintenance.create_database(NonRetryableStore, db_name) - - {:ok, %{"LastDatabaseEtag" => 0}} = Maintenance.database_stats(NonRetryableStore, db_name) - - _ = Maintenance.delete_database(NonRetryableStore, db_name) + {:ok, %{"LastDatabaseEtag" => 0}} = Maintenance.database_stats(NonRetryableStore) end end end From 25d9f536e3ab8d3d690ff7ad5bc779fb0d309d4e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 27 Apr 2022 06:43:09 +0000 Subject: [PATCH 16/23] v0.1.1 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 9b4f491..9a363da 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Ravix.MixProject do def project do [ app: :ravix, - version: "0.1.0", + version: "0.1.1", elixir: "~> 1.13", start_permanent: Mix.env() == :prod, deps: deps(), From 7f86a97c6a8477a13dde94489e5773d7d7a79c70 Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Wed, 27 Apr 2022 21:03:30 +0200 Subject: [PATCH 17/23] Changing Ravix module into an Application, so it will auto-start the registries when imported into other projects --- README.md | 21 +++------------------ lib/ravix.ex | 16 +++++----------- mix.exs | 2 ++ test/connection/connection_test.exs | 5 +---- test/support/test_application.ex | 1 - 5 files changed, 11 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index e93683c..5811f5c 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,6 @@ Add Ravix to your mix.exs dependencies ## Setting up your Repository -  - Create a Ravix Store Module for your repository ```elixir @@ -28,8 +26,6 @@ defmodule YourProject.YourStore do end ``` -  - You can configure your Store in your config.exs files ```elixir @@ -51,17 +47,14 @@ config :ravix, Ravix.Test.Store, } ``` -  - Then you can start the processes in your main supervisor ```elixir defmodule Ravix.TestApplication do - use Supervisor + use Application - def init(_opts) do + def start(_opts, _) do children = [ - {Ravix, [%{}]}, {Ravix.Test.Store, [%{}]} # you can create multiple stores ] @@ -70,19 +63,11 @@ defmodule Ravix.TestApplication do strategy: :one_for_one ) end - - def start_link(init_arg) do - Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__) - end end ``` -  - ## Querying the Database -  - All operations supported by the driver should be executed inside a session, to open a session you can just call the `open_session/0` function from the store you defined. All the changes are only persisted when the function `Ravix.Documents.Session.save_changes/1` is called! @@ -233,6 +218,7 @@ config :ravix, Ravix.Test.Store, ``` ## Ecto + What about querying your RavenDB using Ecto? [Ravix-Ecto](https://github.com/YgorCastor/ravix-ecto) @@ -255,4 +241,3 @@ What about querying your RavenDB using Ecto? [Ravix-Ecto](https://github.com/Ygo * Attachments The driver is working for the basic operations, clustering and resiliency are also implemented. - diff --git a/lib/ravix.ex b/lib/ravix.ex index 62bdbbf..bd50348 100644 --- a/lib/ravix.ex +++ b/lib/ravix.ex @@ -2,28 +2,22 @@ defmodule Ravix do @moduledoc """ Ravix is a RavenDB Driver written in Elixir """ - use Supervisor + use Application @doc """ Initializes three processes registers to facilitate grouping sessions, connections and nodes. - - :sessions = Stores sessions by their UUIDs + - :sessions = Stores sessions by their UUIDs - :connections = Stores the connections processes, based on the Repo Name - :request_executors = Stores the node executors data """ - def init(_opts) do + def start(_type, _args) do children = [ {Registry, [keys: :unique, name: :sessions]}, {Registry, [keys: :unique, name: :connections]}, {Registry, [keys: :unique, name: :request_executors]} ] - Supervisor.init( - children, - strategy: :one_for_one - ) - end - - def start_link(init_arg) do - Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__) + opts = [strategy: :one_for_one, name: Ravix.Supervisor] + Supervisor.start_link(children, opts) end end diff --git a/mix.exs b/mix.exs index 9a363da..123a099 100644 --- a/mix.exs +++ b/mix.exs @@ -33,6 +33,8 @@ defmodule Ravix.MixProject do # Run "mix help compile.app" to learn about applications. def application do [ + mod: {Ravix, []}, + env: [], extra_applications: [:logger] ] end diff --git a/test/connection/connection_test.exs b/test/connection/connection_test.exs index 35ddc40..af331b0 100644 --- a/test/connection/connection_test.exs +++ b/test/connection/connection_test.exs @@ -1,5 +1,5 @@ defmodule Ravix.Connection.ConnectionTest do - use ExUnit.Case + use Ravix.Integration.Case alias Ravix.Connection alias Ravix.Test.Store, as: Store @@ -7,8 +7,6 @@ defmodule Ravix.Connection.ConnectionTest do describe "update_topology/1" do test "Should update the topology correctly" do - %{ravix: start_supervised!(Ravix.TestApplication)} - :ok = Connection.update_topology(Store) {:ok, state} = Connection.fetch_state(Store) @@ -17,7 +15,6 @@ defmodule Ravix.Connection.ConnectionTest do end test "If all nodes are unreachable, the connection will fail" do - _ravix = start_supervised(Ravix) {:error, _} = start_supervised(InvalidStore) end end diff --git a/test/support/test_application.ex b/test/support/test_application.ex index 4c11754..a32b6c4 100644 --- a/test/support/test_application.ex +++ b/test/support/test_application.ex @@ -4,7 +4,6 @@ defmodule Ravix.TestApplication do def init(_opts) do children = [ - {Ravix, [%{}]}, {Ravix.Test.Store, [%{}]}, {Ravix.Test.NonRetryableStore, [%{}]}, {Ravix.Test.OptimisticLockStore, [%{}]} From 2125b7ab1aa00a7ccfe05181256b3608bebd0781 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 27 Apr 2022 19:07:23 +0000 Subject: [PATCH 18/23] v0.1.2 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 123a099..a6b1351 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Ravix.MixProject do def project do [ app: :ravix, - version: "0.1.1", + version: "0.1.2", elixir: "~> 1.13", start_permanent: Mix.env() == :prod, deps: deps(), From 41744d02fdb42275688cc39f4625e86de5ae5d0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 May 2022 11:41:06 +0200 Subject: [PATCH 19/23] Bump timex from 3.7.7 to 3.7.8 (#8) Bumps [timex](https://github.com/bitwalker/timex) from 3.7.7 to 3.7.8. - [Release notes](https://github.com/bitwalker/timex/releases) - [Changelog](https://github.com/bitwalker/timex/blob/main/CHANGELOG.md) - [Commits](https://github.com/bitwalker/timex/commits/3.7.8) --- updated-dependencies: - dependency-name: timex dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index cb6f3b0..a9a1980 100644 --- a/mix.lock +++ b/mix.lock @@ -47,7 +47,7 @@ "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "retry": {:hex, :retry, "0.15.0", "ba6aaeba92905a396c18c299a07e638947b2ba781e914f803202bc1b9ae867c3", [:mix], [], "hexpm", "93d3310bce78c0a30cc94610684340a14adfc9136856a3f662e4d9ce6013c784"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"}, + "timex": {:hex, :timex, "3.7.8", "0e6e8bf7c0aba95f1e13204889b2446e7a5297b1c8e408f15ab58b2c8dc85f81", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8f3b8edc5faab5205d69e5255a1d64a83b190bab7f16baa78aefcb897cf81435"}, "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "version_tasks": {:hex, :version_tasks, "0.12.0", "df384f454369f5f922a541cdc21da2db643c7424c03994986dab2b1702a5b724", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}], "hexpm", "c85e0ec9ad498795609ad849b6dbc668876cecb993fce1f4073016a5b87ee430"}, From 90317d8c5abe57c26b232d37d7f21ea94f97ae45 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 May 2022 11:41:17 +0200 Subject: [PATCH 20/23] Bump ex_doc from 0.28.3 to 0.28.4 (#7) Bumps [ex_doc](https://github.com/elixir-lang/ex_doc) from 0.28.3 to 0.28.4. - [Release notes](https://github.com/elixir-lang/ex_doc/releases) - [Changelog](https://github.com/elixir-lang/ex_doc/blob/main/CHANGELOG.md) - [Commits](https://github.com/elixir-lang/ex_doc/compare/v0.28.3...v0.28.4) --- updated-dependencies: - dependency-name: ex_doc dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index a9a1980..d610933 100644 --- a/mix.lock +++ b/mix.lock @@ -15,7 +15,7 @@ "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, "enum_type": {:hex, :enum_type, "1.1.3", "f4fc5e49e77457b48e58d3caa1626646c6485f61f3f682e800e10bc44e944884", [:mix], [], "hexpm", "474e5e5efb90d9853b5bfd60765b9b6085ef672376f691983c4f705e5da9fad7"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.28.3", "6eea2f69995f5fba94cd6dd398df369fe4e777a47cd887714a0976930615c9e6", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "05387a6a2655b5f9820f3f627450ed20b4325c25977b2ee69bed90af6688e718"}, + "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"}, "excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"}, "fake_server": {:hex, :fake_server, "2.1.0", "aefed08a587e2498fdb39ac9de6f9eabbe7bd83da9801d08d3574d61b7eb03d5", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "3200d57a523b27d2c8ebfc1a80b76697b3c8a06bf9d678d82114f5f98d350c75"}, From 0d6ae7433a9c6bd3bc686226aaa849ae74cf6c09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 May 2022 11:41:28 +0200 Subject: [PATCH 21/23] Bump retry from 0.15.0 to 0.16.0 (#6) Bumps [retry](https://github.com/safwank/ElixirRetry) from 0.15.0 to 0.16.0. - [Release notes](https://github.com/safwank/ElixirRetry/releases) - [Commits](https://github.com/safwank/ElixirRetry/compare/v0.15.0...v0.16.0) --- updated-dependencies: - dependency-name: retry dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index a6b1351..0e4dc3c 100644 --- a/mix.exs +++ b/mix.exs @@ -51,7 +51,7 @@ defmodule Ravix.MixProject do {:morphix, "~> 0.8.1"}, {:timex, "~> 3.7"}, {:tzdata, "~> 1.1"}, - {:retry, "~> 0.15.0"}, + {:retry, "~> 0.16.0"}, {:inflex, "~> 2.1"}, {:gradient, github: "esl/gradient", only: [:dev, :test], runtime: false}, {:elixir_sense, github: "elixir-lsp/elixir_sense", only: [:dev]}, diff --git a/mix.lock b/mix.lock index d610933..0ea7bd0 100644 --- a/mix.lock +++ b/mix.lock @@ -45,7 +45,7 @@ "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, "providers": {:hex, :providers, "1.8.1", "70b4197869514344a8a60e2b2a4ef41ca03def43cfb1712ecf076a0f3c62f083", [:rebar3], [{:getopt, "1.0.1", [hex: :getopt, repo: "hexpm", optional: false]}], "hexpm", "e45745ade9c476a9a469ea0840e418ab19360dc44f01a233304e118a44486ba0"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, - "retry": {:hex, :retry, "0.15.0", "ba6aaeba92905a396c18c299a07e638947b2ba781e914f803202bc1b9ae867c3", [:mix], [], "hexpm", "93d3310bce78c0a30cc94610684340a14adfc9136856a3f662e4d9ce6013c784"}, + "retry": {:hex, :retry, "0.16.0", "bda95a342de242088b2a68e4e826fd2a61f080b5fb2b596bec898e26520d262c", [:mix], [], "hexpm", "bebaa63ebf7c9ebc2741fcdbbda62cf49734a8740ce0a80708b669a595e6be8e"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "timex": {:hex, :timex, "3.7.8", "0e6e8bf7c0aba95f1e13204889b2446e7a5297b1c8e408f15ab58b2c8dc85f81", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8f3b8edc5faab5205d69e5255a1d64a83b190bab7f16baa78aefcb897cf81435"}, "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, From 1a50281f0e66d0aa0f7e7d02b28308f9562d029b Mon Sep 17 00:00:00 2001 From: Ygor Castor Date: Thu, 12 May 2022 20:01:36 +0200 Subject: [PATCH 22/23] [patch] Releasing version --- lib/documents/session/session.ex | 23 +++++++++++++---------- lib/documents/session/session_manager.ex | 21 +++++++++++++++------ test/documents/session/session_test.exs | 14 ++++++++++++++ 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/lib/documents/session/session.ex b/lib/documents/session/session.ex index fe7a136..bf90e5e 100644 --- a/lib/documents/session/session.ex +++ b/lib/documents/session/session.ex @@ -102,21 +102,24 @@ defmodule Ravix.Documents.Session do - entity: the document to store - key: the document key to be used - change_vector: the concurrency change vector + - opts: [ + upsert: If the document is already loaded in the session, it should be upserted + ] ## Returns - `{:ok, Ravix.Documents.Session.State}` - `{:error, cause}` """ - @spec store(binary(), map(), binary() | nil, binary() | nil) :: any - def store(session_id, entity, key \\ nil, change_vector \\ nil) + @spec store(binary(), map(), binary() | nil, binary() | nil, keyword()) :: any + def store(session_id, entity, key \\ nil, change_vector \\ nil, opts \\ []) - def store(_session_id, entity, _key, _change_vector) when entity == nil, + def store(_session_id, entity, _key, _change_vector, _opts) when entity == nil, do: {:error, :null_entity} - def store(session_id, entity, key, change_vector) do + def store(session_id, entity, key, change_vector, opts) do session_id |> session_id() - |> GenServer.call({:store, [entity: entity, key: key, change_vector: change_vector]}) + |> GenServer.call({:store, [entity: entity, key: key, change_vector: change_vector, opts: opts]}) end @doc """ @@ -197,13 +200,13 @@ defmodule Ravix.Documents.Session do end def handle_call( - {:store, [entity: entity, key: key, change_vector: change_vector]}, + {:store, [entity: entity, key: key, change_vector: change_vector, opts: opts]}, _from, %SessionState{} = state ) when key != nil do OK.try do - [entity, updated_state] <- SessionManager.store_entity(state, entity, key, change_vector) + [entity, updated_state] <- SessionManager.store_entity(state, entity, key, change_vector, opts) after {:reply, {:ok, entity}, updated_state} rescue @@ -212,14 +215,14 @@ defmodule Ravix.Documents.Session do end def handle_call( - {:store, [entity: entity, key: _, change_vector: change_vector]}, + {:store, [entity: entity, key: _, change_vector: change_vector, opts: opts]}, _from, %SessionState{} = state ) when is_map_key(entity, :id) do OK.try do [entity, updated_state] <- - SessionManager.store_entity(state, entity, entity.id, change_vector) + SessionManager.store_entity(state, entity, entity.id, change_vector, opts) after {:reply, {:ok, entity}, updated_state} rescue @@ -228,7 +231,7 @@ defmodule Ravix.Documents.Session do end def handle_call( - {:store, [entity: _, key: _, change_vector: _]}, + {:store, [entity: _, key: _, change_vector: _, opts: _]}, _from, %SessionState{} = state ), diff --git a/lib/documents/session/session_manager.ex b/lib/documents/session/session_manager.ex index d1cf164..eb2ab01 100644 --- a/lib/documents/session/session_manager.ex +++ b/lib/documents/session/session_manager.ex @@ -52,22 +52,31 @@ defmodule Ravix.Documents.Session.Manager do end end - @spec store_entity(SessionState.t(), map, any, String.t()) :: {:error, any} | {:ok, [...]} - def store_entity(%SessionState{} = state, entity, key, change_vector) when is_struct(entity) do + @spec store_entity(SessionState.t(), map, any, String.t(), keyword()) :: + {:error, any} | {:ok, [...]} + def store_entity(%SessionState{} = state, entity, key, change_vector, opts) + when is_struct(entity) do entity - |> do_store_entity(key, change_vector, state) + |> do_store_entity(key, change_vector, state, opts) end - def store_entity(%SessionState{} = state, entity, key, change_vector) when is_map(entity) do + def store_entity(%SessionState{} = state, entity, key, change_vector, opts) + when is_map(entity) do entity |> Morphix.atomorphiform!() - |> do_store_entity(key, change_vector, state) + |> do_store_entity(key, change_vector, state, opts) end - defp do_store_entity(entity, key, change_vector, %SessionState{} = state) do + defp do_store_entity(entity, key, change_vector, %SessionState{} = state, opts) do OK.try do _ <- Validations.session_request_limit_reached(state) + _ <- + case Keyword.get(opts, :upsert, true) do + false -> Validations.document_not_stored(state, key) + true -> {:ok, nil} + end + change_vector = case state.conventions.use_optimistic_concurrency do true -> change_vector diff --git a/test/documents/session/session_test.exs b/test/documents/session/session_test.exs index a921137..42957d6 100644 --- a/test/documents/session/session_test.exs +++ b/test/documents/session/session_test.exs @@ -116,6 +116,20 @@ defmodule Ravix.Documents.SessionTest do after end end + + test "If the document is already loaded and it's not an upsert, should return an error" do + any_entity = %{id: UUID.uuid4(), cat_name: Faker.Cat.name()} + + {:error, {:document_already_stored, _}} = + OK.for do + session_id <- Store.open_session() + _ <- Session.store(session_id, any_entity, "custom_key") + _ <- Session.save_changes(session_id) + _ <- Session.load(session_id, "custom_key") + _ <- Session.store(session_id, any_entity, "custom_key", nil, [upsert: false]) + after + end + end end describe "save_changes/1" do From 4f5807c6870d5392c219ac01a787a5bb8c076752 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 May 2022 05:59:14 +0000 Subject: [PATCH 23/23] v0.1.3 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 0e4dc3c..ae278e1 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Ravix.MixProject do def project do [ app: :ravix, - version: "0.1.2", + version: "0.1.3", elixir: "~> 1.13", start_permanent: Mix.env() == :prod, deps: deps(),