defmodule Mongo.Ecto.Connection do @moduledoc false require Logger alias Mongo.Ecto.NormalizedQuery.{ AggregateQuery, CommandQuery, CountQuery, ReadQuery, WriteQuery } alias Mongo.Query def child_spec(opts) do # Rename the `:mongo_url` key so that the driver can parse it opts = Enum.map(opts, fn {:mongo_url, value} -> {:url, value} {key, value} -> {key, value} end) Mongo.child_spec(opts) end ## Worker def init(_config) do end def storage_down(opts) do {:ok, _apps} = Application.ensure_all_started(:mongodb_driver) {:ok, conn} = Mongo.start_link(opts) try do Mongo.command!(conn, dropDatabase: 1) :ok after GenServer.stop(conn) end end def storage_status(opts) do {:ok, _apps} = Application.ensure_all_started(:mongodb_driver) {:ok, conn} = Mongo.start_link(opts) case Mongo.command(conn, ping: true) do {:ok, %{"ok" => 1.0}} -> :up _ -> :down end end ## Callbacks for adapter def read(repo, query, opts \\ []) def read(repo, %ReadQuery{} = query, opts) do projection = Map.put_new(query.projection, :_id, false) opts = [projection: projection, sort: query.order] ++ query.opts ++ opts coll = query.coll query = query.query query(repo, :find, [coll, query], opts) end def read(repo, %CountQuery{} = query, opts) do coll = query.coll opts = query.opts ++ opts query = query.query [%{"value" => query(repo, :count!, [coll, query], opts)}] end def read(repo, %AggregateQuery{} = query, opts) do coll = query.coll opts = query.opts ++ opts pipeline = query.pipeline query(repo, :aggregate, [coll, pipeline], opts) end def delete_all(repo, %WriteQuery{} = query, opts) do coll = query.coll opts = query.opts ++ opts query = query.query %{deleted_count: n} = query(repo, :delete_many!, [coll, query], opts) n end def delete(repo, %WriteQuery{} = query, opts) do coll = query.coll opts = query.opts ++ opts query = query.query case query(repo, :delete_one, [coll, query], opts) do {:ok, %{deleted_count: 1}} -> {:ok, []} {:ok, _} -> {:error, :stale} {:error, error} -> check_constraint_errors(error) end end def update_all(repo, %WriteQuery{} = query, opts) do coll = query.coll command = query.command opts = query.opts ++ opts query = query.query case query(repo, :update_many, [coll, query, command], opts) do {:ok, %Mongo.UpdateResult{modified_count: m} = _result} -> m {:error, error} -> check_constraint_errors(error) end end def update_one(repo, %WriteQuery{} = query, opts) do coll = query.coll command = query.command opts = query.opts ++ opts query = query.query case query(repo, :update_one, [coll, query, command], opts) do {:ok, %{matched_count: 1}} -> {:ok, []} {:ok, _} -> {:error, :stale} {:error, error} -> check_constraint_errors(error) end end def find_one_and_update(repo, %WriteQuery{} = query, opts) do coll = query.coll command = query.command returning = query.returning opts = query.opts ++ opts pk = query.pk query = query.query case query(repo, :find_one_and_update, [coll, query, command], opts) do {:ok, %Mongo.FindAndModifyResult{matched_count: 0, updated_existing: false, upserted_id: nil}} -> {:error, :stale} {:ok, result} -> {:ok, returning_fields(result, returning, pk, opts)} end end def update(repo, %WriteQuery{} = query, opts) do coll = query.coll command = query.command opts = query.opts ++ opts case query(repo, :update, [coll, command], opts) do {:ok, %Mongo.UpdateResult{modified_count: 0, upserted_ids: [_ | _] = upserted_ids}} -> {Enum.count(upserted_ids), nil} {:ok, %Mongo.UpdateResult{modified_count: modified_count}} -> {modified_count, nil} end end def insert(repo, %WriteQuery{} = query, opts) do coll = query.coll command = query.command opts = query.opts ++ opts case query(repo, :insert_one, [coll, command], opts) do {:ok, result} -> {:ok, returning_fields(result, query.returning, query.pk)} {:error, error} -> check_constraint_errors(repo, query, error, {:insert, [repo, query, opts]}, opts) end end # returning_fields/2 extracts the requested returning fields from a Mongo # result struct defp returning_fields(result, fields, primary_key, opts \\ []) defp returning_fields(_result, [], _primary_key, _opts), do: [] defp returning_fields(%Mongo.InsertOneResult{inserted_id: inserted_id}, [pk], pk, _opts), do: Keyword.put([], pk, inserted_id) defp returning_fields( %Mongo.FindAndModifyResult{upserted_id: upserted_id, value: value}, fields, pk, opts ) do fields |> Enum.map(fn ^pk -> case Keyword.get(opts, :return_document) do :after -> {pk, Map.get(value, "_id")} _other -> {pk, upserted_id} end field -> {field, Map.get(value, Atom.to_string(field))} end) end def insert_all(repo, %WriteQuery{} = query, opts) do coll = query.coll command = query.command opts = query.opts ++ opts case query(repo, :insert_many, [coll, command], opts) do {:ok, %{inserted_ids: ids}} -> {Enum.count(ids), nil} {:error, error} -> check_constraint_errors(repo, query, error, {:insert_all, [repo, query, opts]}, opts) end end def command(repo, %CommandQuery{} = query, opts) do command = query.command opts = query.opts ++ opts query(repo, :command!, [command], opts) end def query(adapter_meta, operation, args, opts) do %{pid: pool, telemetry: telemetry, opts: default_opts} = adapter_meta args = [pool] ++ args ++ [with_log(telemetry, args, opts ++ default_opts)] apply(Mongo, operation, args) end defp with_log(telemetry, params, opts) do [log: &log(telemetry, params, &1, opts)] ++ opts end defp log({repo, log, event_name}, _params, entry, opts) do %{ connection_time: query_time, decode_time: decode_time, pool_time: queue_time, idle_time: idle_time, result: result, query: query, params: params } = entry source = Keyword.get(opts, :source) params = Enum.map(params, fn %Ecto.Query.Tagged{value: value} -> value value -> value end) acc = if idle_time, do: [idle_time: idle_time], else: [] measurements = log_measurements( [query_time: query_time, decode_time: decode_time, queue_time: queue_time], 0, acc ) metadata = %{ type: :ecto_sql_query, repo: repo, result: log_result(result), params: params, query: format_query(query, params), source: source, options: Keyword.get(opts, :telemetry_options, []) } if event_name = Keyword.get(opts, :telemetry_event, event_name) do :telemetry.execute(event_name, measurements, metadata) end case Keyword.get(opts, :log, log) do true -> Logger.log( log, fn -> log_iodata(measurements, metadata) end, ansi_color: log_color(query) ) false -> :ok level -> Logger.log( level, fn -> log_iodata(measurements, metadata) end, ansi_color: log_color(query) ) end :ok end defp log_measurements([{_, nil} | rest], total, acc), do: log_measurements(rest, total, acc) defp log_measurements([{key, value} | rest], total, acc), do: log_measurements(rest, total + value, [{key, value} | acc]) defp log_measurements([], total, acc), do: Map.new([total_time: total] ++ acc) # Currently unused defp log_result({:ok, _query, res}), do: {:ok, res} defp log_result(other), do: other defp log_iodata(measurements, metadata) do %{ params: params, query: query, result: result, source: source } = metadata [ "QUERY", ?\s, log_ok_error(result), log_ok_source(source), log_time("db", measurements, :query_time, true), log_time("decode", measurements, :decode_time, false), log_time("queue", measurements, :queue_time, false), log_time("idle", measurements, :idle_time, true), ?\n, query, ?\s, inspect(params, charlists: false) ] end defp log_ok_error({:ok, _res}), do: "OK" defp log_ok_error({:error, _err}), do: "ERROR" defp log_ok_source(nil), do: "" defp log_ok_source(source), do: " source=#{inspect(source)}" defp log_time(label, measurements, key, force) do case measurements do %{^key => time} -> us = System.convert_time_unit(time, :native, :microsecond) ms = div(us, 100) / 10 if force or ms > 0 do [?\s, label, ?=, :io_lib_format.fwrite_g(ms), ?m, ?s] else [] end %{} -> [] end end defp check_constraint_errors(repo, query, error, {retry_function_name, retry_args}, opts) do on_conflict = Keyword.get(opts, :on_conflict) case on_conflict do :nothing -> conflict_targets = opts |> Keyword.get(:conflict_target, []) %{write_errors: [%{"keyPattern" => key_pattern}]} = error conflicting_keys = Map.keys(key_pattern) conflict_targets |> Enum.each(fn conflict_target -> if Atom.to_string(conflict_target) not in conflicting_keys do raise error end end) if query.op == :insert_all do {0, nil} else {:ok, []} end :replace_all -> # Here we have to do a song and dance to delete the offending documents and then reattempt the operation. # The recommended practice is to avoid :replace_all (and other on_conflict options that result in similar need) if allow_unsafe_upserts?(opts) do error.write_errors |> Enum.each(fn write_error -> %{"keyValue" => filter} = write_error query(repo, :delete_one, [query.coll, filter], opts) end) apply(__MODULE__, retry_function_name, retry_args) else raise """ `on_conflict: :replace_all` cannot be accomplished in MongoDB without multiple database calls, not least because MongoDB does not allow the primary key (`_id`) to be replaced. To workaround this issue you may: * Use a different `on_conflict` strategy (this is the safest option) * If you must use `:replace_all`, you may pass an additional `allow_unsafe_upserts: true` option. Passing `allow_unsafe_upserts: true` will cause `mongodb_ecto` to issue multiple database calls in order to resolve conflicts. Since multiple independent calls are involve this cannot be considered safe and should be avoided if possible. """ end end end defp allow_unsafe_upserts?(opts) do allow_unsafe_upserts_option = :allow_unsafe_upserts Application.get_env(:mongodb_ecto, allow_unsafe_upserts_option, false) || Keyword.get(opts, allow_unsafe_upserts_option, false) end # At some point in the past it looks like the MongoDB driver switched from # returning a single `%Mongo.Error{}` to a `%Mongo.WriteError{}` containing # one or more errors in its `write_errors` property. It looks like # `check_constraint_errors` was never really intended to handle that. JP 2021-08-25. defp check_constraint_errors(%Mongo.WriteError{ write_errors: [%{"code" => 11_000, "errmsg" => msg}] }) do {:invalid, [unique: extract_index(msg)]} end defp check_constraint_errors(%Mongo.Error{code: 11_000, message: msg}) do {:invalid, [unique: extract_index(msg)]} end defp check_constraint_errors(other) do raise other end defp extract_index(msg) do parts = String.split(msg, [".$", "index: ", " dup "]) case Enum.reverse(parts) do [_, index | _] -> String.trim(index) _ -> raise "failed to extract index from error message: #{inspect(msg)}" end end def format_constraint_error(index) do %Mongo.Error{ message: "ERROR (11000): could not create unique index \"#{index}\" due to duplicated entry" } end @dialyzer {:no_improper_lists, format_query: 2, format_part: 2} defp format_query(%Query{action: {:command, command}}, []) do ["COMMAND ", inspect(command)] end defp format_query(%Query{action: :find}, [query, projection]) do [ "FIND", # format_part("coll", coll), format_part("query", query), format_part("projection", projection) ] end defp format_query(%Query{action: :insert_one}, [doc]) do [ "INSERT", # format_part("coll", coll), format_part("document", doc) ] end defp format_query(%Query{action: :insert_many}, docs) do [ "INSERT", # format_part("coll", coll), format_part("documents", docs), format_part("many", true) ] end defp format_query(%Query{action: :update_one}, [filter, update]) do [ "UPDATE", # format_part("coll", coll), format_part("filter", filter), format_part("update", update) ] end defp format_query(%Query{action: :update_many}, [filter, update]) do [ "UPDATE", # format_part("coll", coll), format_part("filter", filter), format_part("update", update), format_part("many", true) ] end defp format_query(%Query{action: :delete_one}, [filter]) do [ "DELETE", # format_part("coll", coll), format_part("filter", filter) ] end defp format_query(%Query{action: :delete_many}, [filter]) do [ "DELETE", # format_part("coll", coll), format_part("filter", filter), format_part("many", true) ] end defp format_query(%Query{action: :replace_one}, [filter, doc]) do [ "REPLACE", # format_part("coll", coll), format_part("filter", filter), format_part("document", doc) ] end defp format_query(%Query{action: :get_more}, [cursor]) do [ "GET_MORE", # format_part("coll", coll), format_part("cursor_id", cursor) ] end defp format_query(%Query{action: :get_more}, []) do [ "GET_MORE", # format_part("coll", coll), format_part("cursor_id", "") ] end defp format_query(%Query{action: :kill_cursors}, [cursors]) do ["KILL_CURSORS", format_part("cursor_ids", cursors)] end defp format_query(%Query{action: :kill_cursors}, []) do ["KILL_CURSORS", format_part("cursor_ids", "")] end defp format_query(%Query{action: :wire_version}, []) do ["WIRE_VERSION", format_part("cursor_ids", "")] end defp format_part(name, value) do [" ", name, "=" | inspect(value)] end defp log_color(%Query{action: {:command, _}}), do: :white defp log_color(%Query{action: :find}), do: :cyan defp log_color(%Query{action: :insert_one}), do: :green defp log_color(%Query{action: :insert_many}), do: :green defp log_color(%Query{action: :update_one}), do: :yellow defp log_color(%Query{action: :update_many}), do: :yellow defp log_color(%Query{action: :delete_many}), do: :red defp log_color(%Query{action: :replace_one}), do: :yellow defp log_color(%Query{action: :get_more}), do: :cyan defp log_color(%Query{action: _}), do: nil end