Packages
double_down
0.66.0
0.69.0
0.68.0
0.66.0
0.65.0
0.64.1
0.64.0
0.63.3
0.63.2
0.63.1
0.63.0
0.62.1
0.61.0
0.60.4
0.60.3
0.60.2
0.60.1
0.60.0
0.59.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.0
0.54.0
0.53.0
0.52.3
0.52.2
0.52.1
0.52.0
0.51.0
0.50.1
0.50.0
0.49.0
0.48.1
0.48.0
0.47.2
0.47.1
0.47.0
0.46.3
0.46.2
0.46.1
0.46.0
0.45.0
0.44.0
0.43.0
0.42.0
0.41.1
0.41.0
0.40.0
0.39.0
0.38.0
0.37.2
0.37.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.1
0.31.0
0.30.1
0.30.0
0.29.0
0.28.1
0.28.0
0.27.0
0.26.0
0.24.0
Builds on the Mox pattern — generates behaviours and dispatch facades from `defcallback` declarations — and adds stateful test doubles powerful enough to test Ecto.Repo operations without a database.
Current section
Files
Jump to
Current section
Files
lib/double_down/repo/impl/multi_stepper.ex
# Steps through an Ecto.Multi without a real database.
#
# Shared logic for Test and InMemory adapters. Iterates
# through Multi operations using Ecto.Multi.to_list/1 and
# dispatches each operation to the given `repo_facade` module
# (typically the Repo facade so that :run callbacks can
# call repo.insert/1, repo.get/2, etc.).
#
if Code.ensure_loaded?(Ecto) do
defmodule DoubleDown.Repo.Impl.MultiStepper do
@moduledoc """
Steps through an `Ecto.Multi` without a real database transaction.
Used by `DoubleDown.Repo.Stateless` and `DoubleDown.Repo.OpenInMemory` to execute
Multi operations in order, accumulating a changes map.
The `repo_facade` argument is passed to `:run` callbacks as the
first argument (the "repo"), mirroring how Ecto passes the Repo
module to `Ecto.Multi.run/3` callbacks.
## Return values
* `{:ok, changes}` — all operations succeeded
* `{:error, name, value, changes_so_far}` — an operation failed
"""
@type changes :: %{optional(any()) => any()}
@doc """
Execute all operations in the given `Ecto.Multi`.
`repo_facade` is the module passed to `:run` callbacks as the repo
argument — typically the `Repo facade` module so callbacks can call
`repo.insert/1`, `repo.get/2`, etc.
"""
@spec run(Ecto.Multi.t(), module()) ::
{:ok, changes()}
| {:error, any(), any(), changes()}
def run(%Ecto.Multi{} = multi, repo_facade) do
operations = Ecto.Multi.to_list(multi)
# Pre-check: reject invalid changesets and explicit errors before stepping
case find_pre_check_error(operations) do
{:error, name, value} ->
{:error, name, value, %{}}
:ok ->
step(operations, %{}, repo_facade)
end
end
# -- Pre-check --
defp find_pre_check_error(operations) do
Enum.find_value(operations, :ok, fn
{name, {action, %Ecto.Changeset{valid?: false} = changeset, _opts}}
when action in [:insert, :update, :delete] ->
{:error, name, changeset}
{name, {:error, value}} ->
{:error, name, value}
_ ->
nil
end)
end
# -- Stepping --
defp step([], changes, _repo_facade), do: {:ok, changes}
defp step([{name, operation} | rest], changes, repo_facade) do
case apply_operation(name, operation, changes, repo_facade) do
{:ok, value, _op_changes} ->
step(rest, Map.put(changes, name, value), repo_facade)
{:ok_merge, merged_changes} ->
step(rest, merged_changes, repo_facade)
{:ok_inspect} ->
step(rest, changes, repo_facade)
{:error, value} ->
{:error, name, value, changes}
{:error, failed_name, failed_value, changes_so_far} ->
{:error, failed_name, failed_value, changes_so_far}
end
end
# -- Operation dispatch --
# Changeset operations (insert, update, delete)
defp apply_operation(_name, {:insert, changeset, _opts}, _changes, repo_facade) do
apply_changeset_op(repo_facade, :insert, changeset)
end
defp apply_operation(_name, {:update, changeset, _opts}, _changes, repo_facade) do
apply_changeset_op(repo_facade, :update, changeset)
end
defp apply_operation(_name, {:delete, changeset_or_struct, _opts}, _changes, repo_facade) do
apply_changeset_op(repo_facade, :delete, changeset_or_struct)
end
# Run operations (arbitrary functions)
defp apply_operation(_name, {:run, fun}, changes, repo_facade)
when is_function(fun, 2) do
case fun.(repo_facade, changes) do
{:ok, value} -> {:ok, value, changes}
{:error, value} -> {:error, value}
end
end
defp apply_operation(_name, {:run, {mod, fun, args}}, changes, repo_facade) do
case apply(mod, fun, [repo_facade, changes | args]) do
{:ok, value} -> {:ok, value, changes}
{:error, value} -> {:error, value}
end
end
# Put (static value)
defp apply_operation(_name, {:put, value}, changes, _repo_facade) do
{:ok, value, changes}
end
# Error (explicit failure — should be caught by pre-check, but handle anyway)
defp apply_operation(_name, {:error, value}, _changes, _repo_facade) do
{:error, value}
end
# Inspect (debug logging) — mirrors Ecto.Multi.inspect/2 behaviour
# credo:disable-for-next-line Credo.Check.Warning.IoInspect
defp apply_operation(_name, {:inspect, opts}, changes, _repo_facade) do
if opts[:only] do
# credo:disable-for-next-line Credo.Check.Warning.IoInspect
changes |> Map.take(List.wrap(opts[:only])) |> IO.inspect(opts)
else
# credo:disable-for-next-line Credo.Check.Warning.IoInspect
IO.inspect(changes, opts)
end
{:ok_inspect}
end
# Merge (dynamic Multi composition)
defp apply_operation(_name, {:merge, merge_fn}, changes, repo_facade)
when is_function(merge_fn, 1) do
sub_multi = merge_fn.(changes)
apply_merge(sub_multi, changes, repo_facade)
end
defp apply_operation(_name, {:merge, {mod, fun, args}}, changes, repo_facade) do
sub_multi = apply(mod, fun, [changes | args])
apply_merge(sub_multi, changes, repo_facade)
end
# Bulk operations — route through repo_facade like changeset ops
defp apply_operation(_name, {:insert_all, source, entries, opts}, changes, repo_facade) do
result = apply(repo_facade, :insert_all, [source, entries, opts])
{:ok, result, changes}
end
defp apply_operation(_name, {:update_all, query, updates, opts}, changes, repo_facade) do
result = apply(repo_facade, :update_all, [query, updates, opts])
{:ok, result, changes}
end
defp apply_operation(_name, {:delete_all, query, opts}, changes, repo_facade) do
result = apply(repo_facade, :delete_all, [query, opts])
{:ok, result, changes}
end
# -- Helpers --
defp apply_changeset_op(repo_facade, action, %Ecto.Changeset{} = changeset) do
case apply(repo_facade, action, [changeset]) do
{:ok, value} -> {:ok, value, %{}}
{:error, value} -> {:error, value}
end
end
defp apply_changeset_op(repo_facade, :delete, struct) do
case apply(repo_facade, :delete, [struct]) do
{:ok, value} -> {:ok, value, %{}}
{:error, value} -> {:error, value}
end
end
defp apply_merge(sub_multi, changes, repo_facade) do
case run(sub_multi, repo_facade) do
{:ok, sub_changes} ->
{:ok_merge, Map.merge(changes, sub_changes)}
{:error, name, value, sub_changes} ->
{:error, name, value, Map.merge(changes, sub_changes)}
end
end
end
end