Current section

Files

Jump to
double_down lib double_down repo impl multi_stepper.ex
Raw

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.Stub` 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 — insert_all, update_all, delete_all
# Test/InMemory adapters don't support real bulk ops; return {0, nil}
defp apply_operation(_name, {:insert_all, _source, _entries, _opts}, changes, _repo_facade) do
{:ok, {0, nil}, changes}
end
defp apply_operation(_name, {:update_all, _query, _updates, _opts}, changes, _repo_facade) do
{:ok, {0, nil}, changes}
end
defp apply_operation(_name, {:delete_all, _query, _opts}, changes, _repo_facade) do
{:ok, {0, nil}, 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