Skip to content

Commit 7ffb885

Browse files
authored
Compatibility changes to support mongodb_ecto adapter (#227)
* Add BSON encoders for Elixir Date/NaiveDateTime * Return FindAndModifyResult struct from appropriate operations * Fix conflation of application `log` env var and function option of the same name The application env variable called `log` is meant to be either a boolean or atom log level, whereas the function option called `log` is potentially a function or MFA tuple that is passed down to DBConnection. * Add generic Mongo.update/4 function This function is copied from the older `mongodb` driver for compatibility with the ecto adapter * Update tests for functions returning FindAndModifyResult * Mix format * Update array_filters test for FindAndModifyResult structs
1 parent 3f3c354 commit 7ffb885

File tree

4 files changed

+153
-17
lines changed

4 files changed

+153
-17
lines changed

lib/bson/encoder.ex

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ defmodule BSON.Encoder do
3636
<<unix_ms::int64()>>
3737
end
3838

39+
def encode(%Date{} = date) do
40+
unix_ms =
41+
NaiveDateTime.from_erl!({Date.to_erl(date), {0, 0, 0}}, 0, Calendar.ISO)
42+
|> DateTime.from_naive!("Etc/UTC")
43+
|> DateTime.to_unix(:millisecond)
44+
45+
<<unix_ms::int64()>>
46+
end
47+
48+
def encode(%NaiveDateTime{} = datetime) do
49+
unix_ms =
50+
datetime
51+
|> DateTime.from_naive!("Etc/UTC")
52+
|> DateTime.to_unix(:millisecond)
53+
54+
<<unix_ms::int64()>>
55+
end
56+
3957
def encode(%BSON.Regex{pattern: pattern, options: options}),
4058
do: [cstring(pattern) | cstring(options)]
4159

@@ -152,6 +170,8 @@ defmodule BSON.Encoder do
152170
defp type(%BSON.Binary{}), do: @type_binary
153171
defp type(%BSON.ObjectId{}), do: @type_objectid
154172
defp type(%DateTime{}), do: @type_datetime
173+
defp type(%NaiveDateTime{}), do: @type_datetime
174+
defp type(%Date{}), do: @type_datetime
155175
defp type(%BSON.Regex{}), do: @type_regex
156176
defp type(%BSON.JavaScript{scope: nil}), do: @type_js
157177
defp type(%BSON.JavaScript{}), do: @type_js_scope

lib/mongo.ex

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,13 @@ defmodule Mongo do
503503
)
504504

505505
with {:ok, doc} <- issue_command(topology_pid, cmd, :write, opts) do
506-
{:ok, doc["value"]}
506+
{:ok,
507+
%Mongo.FindAndModifyResult{
508+
value: doc["value"],
509+
matched_count: doc["lastErrorObject"]["n"],
510+
updated_existing: doc["lastErrorObject"]["updatedExisting"],
511+
upserted_id: doc["lastErrorObject"]["upserted"]
512+
}}
507513
end
508514
end
509515

@@ -559,7 +565,15 @@ defmodule Mongo do
559565
~w(bypass_document_validation max_time projection return_document sort upsert collation)a
560566
)
561567

562-
with {:ok, doc} <- issue_command(topology_pid, cmd, :write, opts), do: {:ok, doc["value"]}
568+
with {:ok, doc} <- issue_command(topology_pid, cmd, :write, opts) do
569+
{:ok,
570+
%Mongo.FindAndModifyResult{
571+
value: doc["value"],
572+
matched_count: doc["lastErrorObject"]["n"],
573+
updated_existing: doc["lastErrorObject"]["updatedExisting"],
574+
upserted_id: doc["lastErrorObject"]["upserted"]
575+
}}
576+
end
563577
end
564578

565579
defp should_return_new(:after), do: true
@@ -1094,6 +1108,86 @@ defmodule Mongo do
10941108
bangify(update_many(topology_pid, coll, filter, update, opts))
10951109
end
10961110

1111+
@doc """
1112+
Performs one or more update operations.
1113+
1114+
This function is especially useful for more complex update operations (e.g.
1115+
upserting multiple documents). For more straightforward use cases you may
1116+
prefer to use these higher level APIs:
1117+
1118+
* `update_one/5`
1119+
* `update_one!/5`
1120+
* `update_many/5`
1121+
* `update_many!5`
1122+
1123+
Each update in `updates` may be specified using either the short-hand
1124+
Mongo-style syntax (in reference to their docs) or using a long-hand, Elixir
1125+
friendly syntax.
1126+
1127+
See
1128+
https://docs.mongodb.com/manual/reference/command/update/#update-statements
1129+
1130+
e.g. long-hand `query` becomes short-hand `q`, snake case `array_filters`
1131+
becomes `arrayFilters`
1132+
"""
1133+
def update(topology_pid, coll, updates, opts) do
1134+
write_concern =
1135+
filter_nils(%{
1136+
w: Keyword.get(opts, :w),
1137+
j: Keyword.get(opts, :j),
1138+
wtimeout: Keyword.get(opts, :wtimeout)
1139+
})
1140+
1141+
normalised_updates = updates |> normalise_updates()
1142+
1143+
cmd =
1144+
[
1145+
update: coll,
1146+
updates: normalised_updates,
1147+
ordered: Keyword.get(opts, :ordered),
1148+
writeConcern: write_concern,
1149+
bypassDocumentValidation: Keyword.get(opts, :bypass_document_validation)
1150+
]
1151+
|> filter_nils()
1152+
1153+
with {:ok, doc} <- issue_command(topology_pid, cmd, :write, opts) do
1154+
case doc do
1155+
%{"writeErrors" => write_errors} ->
1156+
{:error, %Mongo.WriteError{n: doc["n"], ok: doc["ok"], write_errors: write_errors}}
1157+
1158+
%{"n" => n, "nModified" => n_modified} ->
1159+
{:ok,
1160+
%Mongo.UpdateResult{
1161+
matched_count: n,
1162+
modified_count: n_modified,
1163+
upserted_ids: filter_upsert_ids(doc["upserted"])
1164+
}}
1165+
1166+
%{"ok" => ok} when ok == 1 ->
1167+
{:ok, %Mongo.UpdateResult{acknowledged: false}}
1168+
end
1169+
end
1170+
end
1171+
1172+
defp normalise_updates([[{_, _} | _] | _] = updates) do
1173+
updates
1174+
|> Enum.map(&normalise_update/1)
1175+
end
1176+
1177+
defp normalise_updates(updates), do: normalise_updates([updates])
1178+
1179+
defp normalise_update(update) do
1180+
update
1181+
|> Enum.map(fn
1182+
{:query, query} -> {:q, query}
1183+
{:update, update} -> {:u, update}
1184+
{:updates, update} -> {:u, update}
1185+
{:array_filters, array_filters} -> {:arrayFilters, array_filters}
1186+
other -> other
1187+
end)
1188+
|> filter_nils()
1189+
end
1190+
10971191
##
10981192
# Calls the update command:
10991193
#
@@ -1804,9 +1898,7 @@ defmodule Mongo do
18041898

18051899
:telemetry.execute([:mongodb_driver, :execution], %{duration: duration}, metadata)
18061900

1807-
log = Application.get_env(:mongodb_driver, :log, false)
1808-
1809-
case Keyword.get(opts, :log, log) do
1901+
case Application.get_env(:mongodb_driver, :log, false) do
18101902
true ->
18111903
Logger.log(:info, fn -> log_iodata(command, collection, params, duration) end, ansi_color: command_color(command))
18121904

lib/mongo/results.ex

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,30 @@ defmodule Mongo.UpdateResult do
6363
defstruct acknowledged: true, matched_count: 0, modified_count: 0, upserted_ids: []
6464
end
6565

66+
defmodule Mongo.FindAndModifyResult do
67+
@moduledoc """
68+
The successful result struct of `Mongo.find_one_and_*` functions, which under
69+
the hood use Mongo's `findAndModify` API.
70+
71+
See <https://docs.mongodb.com/manual/reference/command/findAndModify/> for
72+
more information.
73+
"""
74+
75+
@type t :: %__MODULE__{
76+
value: BSON.document(),
77+
matched_count: non_neg_integer(),
78+
upserted_id: String.t(),
79+
updated_existing: boolean()
80+
}
81+
82+
defstruct [
83+
:value,
84+
:matched_count,
85+
:upserted_id,
86+
:updated_existing
87+
]
88+
end
89+
6690
defmodule Mongo.BulkWriteResult do
6791
@moduledoc """
6892
The successful result struct of `Mongo.BulkWrite.write`. Its fields are:

test/mongo_test.exs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ defmodule Mongo.Test do
223223
assert {:ok, _} = Mongo.insert_one(c.pid, coll, %{foo: 42, bar: 1})
224224

225225
# defaults
226-
assert {:ok, value} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 42}, %{"$set" => %{bar: 2}})
226+
assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 42}, %{"$set" => %{bar: 2}})
227227
assert %{"bar" => 1} = value, "Should return original document by default"
228228

229229
# should raise if we don't have atomic operators
@@ -232,31 +232,31 @@ defmodule Mongo.Test do
232232
end
233233

234234
# return_document = :after
235-
assert {:ok, value} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 42}, %{"$set" => %{bar: 3}}, return_document: :after)
235+
assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 42}, %{"$set" => %{bar: 3}}, return_document: :after)
236236
assert %{"bar" => 3} = value, "Should return modified doc"
237237

238238
# projection
239-
assert {:ok, value} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 42}, %{"$set" => %{bar: 3}}, projection: %{"bar" => 1})
239+
assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 42}, %{"$set" => %{bar: 3}}, projection: %{"bar" => 1})
240240
assert Map.get(value, "foo") == nil, "Should respect the projection"
241241

242242
# sort
243243
assert {:ok, _} = Mongo.insert_one(c.pid, coll, %{foo: 42, bar: 10})
244-
assert {:ok, value} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 42}, %{"$set" => %{baz: 1}}, sort: %{"bar" => -1}, return_document: :after)
244+
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)
245245
assert %{"bar" => 10, "baz" => 1} = value, "Should respect the sort"
246246

247247
# upsert
248-
assert {:ok, value} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 43}, %{"$set" => %{baz: 1}}, upsert: true, return_document: :after)
248+
assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_update(c.pid, coll, %{"foo" => 43}, %{"$set" => %{baz: 1}}, upsert: true, return_document: :after)
249249
assert %{"foo" => 43, "baz" => 1} = value, "Should upsert"
250250

251251
# array_filters
252252
assert {:ok, _} = Mongo.insert_one(c.pid, coll, %{foo: 44, things: [%{id: "123", name: "test"}, %{id: "456", name: "not test"}]})
253-
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)
253+
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)
254254
assert %{"foo" => 44, "things" => [%{"id" => "123", "name" => "new"}, %{"id" => "456", "name" => "not test"}]} = value, "Should leverage array filters"
255255

256256
# don't find return {:ok, nil}
257-
assert {:ok, nil} == Mongo.find_one_and_update(c.pid, coll, %{"number" => 666}, %{"$set" => %{title: "the number of the beast"}})
257+
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"}})
258258

259-
assert {:ok, nil} == Mongo.find_one_and_update(c.pid, "coll_that_doesnt_exist", %{"number" => 666}, %{"$set" => %{title: "the number of the beast"}})
259+
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"}})
260260

261261
# wrong parameter
262262
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
272272
end
273273

274274
# defaults
275-
assert {:ok, value} = Mongo.find_one_and_replace(c.pid, coll, %{"foo" => 42}, %{bar: 2})
275+
assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_replace(c.pid, coll, %{"foo" => 42}, %{bar: 2})
276276
assert %{"foo" => 42, "bar" => 1} = value, "Should return original document by default"
277277

278278
# return_document = :after
279279
assert {:ok, _} = Mongo.insert_one(c.pid, coll, %{foo: 43, bar: 1})
280-
assert {:ok, value} = Mongo.find_one_and_replace(c.pid, coll, %{"foo" => 43}, %{bar: 3}, return_document: :after)
280+
assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_replace(c.pid, coll, %{"foo" => 43}, %{bar: 3}, return_document: :after)
281281
assert %{"bar" => 3} = value, "Should return modified doc"
282282
assert match?(%{"foo" => 43}, value) == false, "Should replace document"
283283

284284
# projection
285285
assert {:ok, _} = Mongo.insert_one(c.pid, coll, %{foo: 44, bar: 1})
286-
assert {:ok, value} = Mongo.find_one_and_replace(c.pid, coll, %{"foo" => 44}, %{foo: 44, bar: 3}, return_document: :after, projection: %{bar: 1})
286+
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})
287287
assert Map.get(value, "foo") == nil, "Should respect the projection"
288288

289289
# sort
@@ -295,7 +295,7 @@ defmodule Mongo.Test do
295295

296296
# upsert
297297
assert [] = Mongo.find(c.pid, coll, %{upsertedDocument: true}) |> Enum.to_list()
298-
assert {:ok, value} = Mongo.find_one_and_replace(c.pid, coll, %{"upsertedDocument" => true}, %{"upsertedDocument" => true}, upsert: true, return_document: :after)
298+
assert {:ok, %Mongo.FindAndModifyResult{value: value}} = Mongo.find_one_and_replace(c.pid, coll, %{"upsertedDocument" => true}, %{"upsertedDocument" => true}, upsert: true, return_document: :after)
299299
assert %{"upsertedDocument" => true} = value, "Should upsert"
300300
assert [%{"upsertedDocument" => true}] = Mongo.find(c.pid, coll, %{upsertedDocument: true}) |> Enum.to_list()
301301
end

0 commit comments

Comments
 (0)