Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compatibility changes to support mongodb_ecto adapter #227

Merged
merged 8 commits into from
Jan 13, 2024
20 changes: 20 additions & 0 deletions lib/bson/encoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ defmodule BSON.Encoder do
<<unix_ms::int64()>>
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)

<<unix_ms::int64()>>
end

def encode(%NaiveDateTime{} = datetime) do
unix_ms =
datetime
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.to_unix(:millisecond)

<<unix_ms::int64()>>
end

def encode(%BSON.Regex{pattern: pattern, options: options}),
do: [cstring(pattern) | cstring(options)]

Expand Down Expand Up @@ -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
Expand Down
102 changes: 97 additions & 5 deletions lib/mongo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
#
Expand Down Expand Up @@ -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))

Expand Down
24 changes: 24 additions & 0 deletions lib/mongo/results.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://docs.mongodb.com/manual/reference/command/findAndModify/> 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:
Expand Down
24 changes: 12 additions & 12 deletions test/mongo_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"}})
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading