How to execute Ecto.Multi clause conditionally?

Background

I have 3 queries (A, B and C) that need to be run in a transaction. Query A is always run, as well as query B. Query C is run only if query B returns not empty.

These need to be in a transaction, because if query C fails, I want to rollback any changed made by query A.

Code

Currently my code looks like this:

query_A = 
    # some query 

query_B =
     # some other query

query_C =
      # final query

Multi.new()
    |> Multi.update_all(:a, query_A,
      set: [update_value: true]
    )
    |> Multi.all(:all, query_B)
    |> Multi.update_all(:c, query_C,
      set: [update_other_value: true]
    )
    |> Repo.transaction()

Problems

There are several problems here:

  1. Multi.all does not have an atom associated, so if this operation fails I don’t know how to identify it
  2. I am always running :c. This is not what I want, I only want to do this if the previous Multi.all returns non-empty.

Is it possible to achieve this, using Ecto.Multi? I have seen Multi.run but I am unsure on how to make it fit here.

It does. It’s the :all you provided.

You can use Ecto.Multi.merge to merge additional steps based on the previous steps results.

I see, so this is not the same as Repo.all(:all, Posts), here I pass the name:

https://hexdocs.pm/ecto/Ecto.Multi.html#all/4

I see you mean this:

https://hexdocs.pm/ecto/Ecto.Multi.html#merge/2

But I fail to grasp how I could make this a conditional.

Multi.new()
    |> Multi.update_all(:a, query_A,
      set: [update_value: true]
    )
    |> Multi.all(:all, query_B)
    |> Multi.merge(fn something -> 

        # if not Enum.empty?(something) ???   
        Multi.new()
        |> Multi.update_all(:c, query_C,
            set: [update_other_value: true]
        )
    end)
    |> Repo.transaction()
if Enum.empty?(something.all) do
  …
else
  …
end
1 Like

And in the case where nothing happens, should I simply return Multi.new() as a noop?

Exactly.

1 Like

You can pipe it into case:

|> case do
Empty → Repo transaction
Result → do query c |> Repo transaction
end

That doesn’t work, as before running Repo.transaction there’s no way to know if a certain query result will be empty or not.

1 Like

I have built this query which now seems to work as intended:

Multi.new()
    |> Multi.update_all(:a, query_A, set:  [update_value: true])
    |> Multi.all(:b, query_B)
    |> Multi.merge(fn result ->     
      case result.b do
        [] ->
          Multi.new()

        _ ->
          Multi.new()
          |> Multi.update_all(:c, query_C, set: [update_other_value: true])
      end
    end)
    |> Repo.transaction()

It is important to notice that query_A should not affect query_B. In case this happens, query_B should be done first (as it is a read only operation).

Thank you all for the help!

2 Likes