diff --git a/lib/bson/encoder.ex b/lib/bson/encoder.ex index 4ca0cdbc..63c4b686 100644 --- a/lib/bson/encoder.ex +++ b/lib/bson/encoder.ex @@ -36,6 +36,24 @@ defmodule BSON.Encoder do <> end + def encode(%Date{} = date) do + unix_ms = + NaiveDateTime.from_erl!({Date.to_erl(date), {0, 0, 0}}, 0, Calendar.ISO) + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix(:millisecond) + + <> + end + + def encode(%NaiveDateTime{} = datetime) do + unix_ms = + datetime + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix(:millisecond) + + <> + end + def encode(%BSON.Regex{pattern: pattern, options: options}), do: [cstring(pattern) | cstring(options)] @@ -152,6 +170,8 @@ defmodule BSON.Encoder do defp type(%BSON.Binary{}), do: @type_binary defp type(%BSON.ObjectId{}), do: @type_objectid defp type(%DateTime{}), do: @type_datetime + defp type(%NaiveDateTime{}), do: @type_datetime + defp type(%Date{}), do: @type_datetime defp type(%BSON.Regex{}), do: @type_regex defp type(%BSON.JavaScript{scope: nil}), do: @type_js defp type(%BSON.JavaScript{}), do: @type_js_scope diff --git a/lib/mongo.ex b/lib/mongo.ex index 4200f8ca..c75fe250 100644 --- a/lib/mongo.ex +++ b/lib/mongo.ex @@ -503,7 +503,13 @@ defmodule Mongo do ) with {:ok, doc} <- issue_command(topology_pid, cmd, :write, opts) do - {:ok, doc["value"]} + {:ok, + %Mongo.FindAndModifyResult{ + value: doc["value"], + matched_count: doc["lastErrorObject"]["n"], + updated_existing: doc["lastErrorObject"]["updatedExisting"], + upserted_id: doc["lastErrorObject"]["upserted"] + }} end end @@ -559,7 +565,15 @@ defmodule Mongo do ~w(bypass_document_validation max_time projection return_document sort upsert collation)a ) - with {:ok, doc} <- issue_command(topology_pid, cmd, :write, opts), do: {:ok, doc["value"]} + with {:ok, doc} <- issue_command(topology_pid, cmd, :write, opts) do + {:ok, + %Mongo.FindAndModifyResult{ + value: doc["value"], + matched_count: doc["lastErrorObject"]["n"], + updated_existing: doc["lastErrorObject"]["updatedExisting"], + upserted_id: doc["lastErrorObject"]["upserted"] + }} + end end defp should_return_new(:after), do: true @@ -1094,6 +1108,86 @@ defmodule Mongo do bangify(update_many(topology_pid, coll, filter, update, opts)) end + @doc """ + Performs one or more update operations. + + This function is especially useful for more complex update operations (e.g. + upserting multiple documents). For more straightforward use cases you may + prefer to use these higher level APIs: + + * `update_one/5` + * `update_one!/5` + * `update_many/5` + * `update_many!5` + + Each update in `updates` may be specified using either the short-hand + Mongo-style syntax (in reference to their docs) or using a long-hand, Elixir + friendly syntax. + + See + https://docs.mongodb.com/manual/reference/command/update/#update-statements + + e.g. long-hand `query` becomes short-hand `q`, snake case `array_filters` + becomes `arrayFilters` + """ + def update(topology_pid, coll, updates, opts) do + write_concern = + filter_nils(%{ + w: Keyword.get(opts, :w), + j: Keyword.get(opts, :j), + wtimeout: Keyword.get(opts, :wtimeout) + }) + + normalised_updates = updates |> normalise_updates() + + cmd = + [ + update: coll, + updates: normalised_updates, + ordered: Keyword.get(opts, :ordered), + writeConcern: write_concern, + bypassDocumentValidation: Keyword.get(opts, :bypass_document_validation) + ] + |> filter_nils() + + with {:ok, doc} <- issue_command(topology_pid, cmd, :write, opts) do + case doc do + %{"writeErrors" => write_errors} -> + {:error, %Mongo.WriteError{n: doc["n"], ok: doc["ok"], write_errors: write_errors}} + + %{"n" => n, "nModified" => n_modified} -> + {:ok, + %Mongo.UpdateResult{ + matched_count: n, + modified_count: n_modified, + upserted_ids: filter_upsert_ids(doc["upserted"]) + }} + + %{"ok" => ok} when ok == 1 -> + {:ok, %Mongo.UpdateResult{acknowledged: false}} + end + end + end + + defp normalise_updates([[{_, _} | _] | _] = updates) do + updates + |> Enum.map(&normalise_update/1) + end + + defp normalise_updates(updates), do: normalise_updates([updates]) + + defp normalise_update(update) do + update + |> Enum.map(fn + {:query, query} -> {:q, query} + {:update, update} -> {:u, update} + {:updates, update} -> {:u, update} + {:array_filters, array_filters} -> {:arrayFilters, array_filters} + other -> other + end) + |> filter_nils() + end + ## # Calls the update command: # @@ -1804,9 +1898,7 @@ defmodule Mongo do :telemetry.execute([:mongodb_driver, :execution], %{duration: duration}, metadata) - log = Application.get_env(:mongodb_driver, :log, false) - - case Keyword.get(opts, :log, log) do + case Application.get_env(:mongodb_driver, :log, false) do true -> Logger.log(:info, fn -> log_iodata(command, collection, params, duration) end, ansi_color: command_color(command)) diff --git a/lib/mongo/results.ex b/lib/mongo/results.ex index 03a223b1..a369a848 100644 --- a/lib/mongo/results.ex +++ b/lib/mongo/results.ex @@ -63,6 +63,30 @@ defmodule Mongo.UpdateResult do defstruct acknowledged: true, matched_count: 0, modified_count: 0, upserted_ids: [] end +defmodule Mongo.FindAndModifyResult do + @moduledoc """ + The successful result struct of `Mongo.find_one_and_*` functions, which under + the hood use Mongo's `findAndModify` API. + + See for + more information. + """ + + @type t :: %__MODULE__{ + value: BSON.document(), + matched_count: non_neg_integer(), + upserted_id: String.t(), + updated_existing: boolean() + } + + defstruct [ + :value, + :matched_count, + :upserted_id, + :updated_existing + ] +end + defmodule Mongo.BulkWriteResult do @moduledoc """ The successful result struct of `Mongo.BulkWrite.write`. Its fields are: diff --git a/test/mongo_test.exs b/test/mongo_test.exs index fb91014a..ed4d59e0 100644 --- a/test/mongo_test.exs +++ b/test/mongo_test.exs @@ -223,7 +223,7 @@ defmodule Mongo.Test do assert {:ok, _} = Mongo.insert_one(c.pid, coll, %{foo: 42, bar: 1}) # defaults - assert {:ok, value} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 42}, %{"$set" => %{bar: 2}}) + assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 42}, %{"$set" => %{bar: 2}}) assert %{"bar" => 1} = value, "Should return original document by default" # should raise if we don't have atomic operators @@ -232,31 +232,31 @@ defmodule Mongo.Test do end # return_document = :after - assert {:ok, value} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 42}, %{"$set" => %{bar: 3}}, return_document: :after) + assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 42}, %{"$set" => %{bar: 3}}, return_document: :after) assert %{"bar" => 3} = value, "Should return modified doc" # projection - assert {:ok, value} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 42}, %{"$set" => %{bar: 3}}, projection: %{"bar" => 1}) + assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 42}, %{"$set" => %{bar: 3}}, projection: %{"bar" => 1}) assert Map.get(value, "foo") == nil, "Should respect the projection" # sort assert {:ok, _} = Mongo.insert_one(c.pid, coll, %{foo: 42, bar: 10}) - assert {:ok, value} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 42}, %{"$set" => %{baz: 1}}, sort: %{"bar" => -1}, return_document: :after) + assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 42}, %{"$set" => %{baz: 1}}, sort: %{"bar" => -1}, return_document: :after) assert %{"bar" => 10, "baz" => 1} = value, "Should respect the sort" # upsert - assert {:ok, value} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 43}, %{"$set" => %{baz: 1}}, upsert: true, return_document: :after) + assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 43}, %{"$set" => %{baz: 1}}, upsert: true, return_document: :after) assert %{"foo" => 43, "baz" => 1} = value, "Should upsert" # array_filters assert {:ok, _} = Mongo.insert_one(c.pid, coll, %{foo: 44, things: [%{id: "123", name: "test"}, %{id: "456", name: "not test"}]}) - assert {:ok, value} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 44}, %{"$set" => %{"things.$[sub].name" => "new"}}, array_filters: [%{"sub.id" => "123"}], return_document: :after) + assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 44}, %{"$set" => %{"things.$[sub].name" => "new"}}, array_filters: [%{"sub.id" => "123"}], return_document: :after) assert %{"foo" => 44, "things" => [%{"id" => "123", "name" => "new"}, %{"id" => "456", "name" => "not test"}]} = value, "Should leverage array filters" # don't find return {:ok, nil} - assert {:ok, nil} == Mongo.find_one_and_update(c.pid, coll, %{"number" => 666}, %{"$set" => %{title: "the number of the beast"}}) + assert {:ok, %Mongo.FindAndModifyResult{matched_count: 0, updated_existing: false, value: nil}} == Mongo.find_one_and_update(c.pid, coll, %{"number" => 666}, %{"$set" => %{title: "the number of the beast"}}) - assert {:ok, nil} == Mongo.find_one_and_update(c.pid, "coll_that_doesnt_exist", %{"number" => 666}, %{"$set" => %{title: "the number of the beast"}}) + assert {:ok, %Mongo.FindAndModifyResult{matched_count: 0, updated_existing: false, value: nil}} == Mongo.find_one_and_update(c.pid, "coll_that_doesnt_exist", %{"number" => 666}, %{"$set" => %{title: "the number of the beast"}}) # wrong parameter assert {:error, %Mongo.Error{}} = Mongo.find_one_and_update(c.pid, 2, %{"number" => 666}, %{"$set" => %{title: "the number of the beast"}}) @@ -272,18 +272,18 @@ defmodule Mongo.Test do end # defaults - assert {:ok, value} = Mongo.find_one_and_replace(c.pid, coll, %{"foo" => 42}, %{bar: 2}) + assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_replace(c.pid, coll, %{"foo" => 42}, %{bar: 2}) assert %{"foo" => 42, "bar" => 1} = value, "Should return original document by default" # return_document = :after assert {:ok, _} = Mongo.insert_one(c.pid, coll, %{foo: 43, bar: 1}) - assert {:ok, value} = Mongo.find_one_and_replace(c.pid, coll, %{"foo" => 43}, %{bar: 3}, return_document: :after) + assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_replace(c.pid, coll, %{"foo" => 43}, %{bar: 3}, return_document: :after) assert %{"bar" => 3} = value, "Should return modified doc" assert match?(%{"foo" => 43}, value) == false, "Should replace document" # projection assert {:ok, _} = Mongo.insert_one(c.pid, coll, %{foo: 44, bar: 1}) - assert {:ok, value} = Mongo.find_one_and_replace(c.pid, coll, %{"foo" => 44}, %{foo: 44, bar: 3}, return_document: :after, projection: %{bar: 1}) + assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_replace(c.pid, coll, %{"foo" => 44}, %{foo: 44, bar: 3}, return_document: :after, projection: %{bar: 1}) assert Map.get(value, "foo") == nil, "Should respect the projection" # sort @@ -295,7 +295,7 @@ defmodule Mongo.Test do # upsert assert [] = Mongo.find(c.pid, coll, %{upsertedDocument: true}) |> Enum.to_list() - assert {:ok, value} = Mongo.find_one_and_replace(c.pid, coll, %{"upsertedDocument" => true}, %{"upsertedDocument" => true}, upsert: true, return_document: :after) + assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_replace(c.pid, coll, %{"upsertedDocument" => true}, %{"upsertedDocument" => true}, upsert: true, return_document: :after) assert %{"upsertedDocument" => true} = value, "Should upsert" assert [%{"upsertedDocument" => true}] = Mongo.find(c.pid, coll, %{upsertedDocument: true}) |> Enum.to_list() end