Conditional Ecto.Multi if param value is set and not nil or empty string

I have this function that has an Ecto.Multi operation where I include a Multi.insert if the function argument (a map) field is not nil or empty.

    review_changeset =
      %Review{}
      |> Review.changeset(attrs)

    Multi.new()
    |> Multi.insert(:review, review_changeset)
    |> Multi.run(:review_photo, fn transaction_repo, %{review: %Review{id: review_id}} ->
      case Map.fetch(attrs, "photo_url") do
        {:ok, nil} ->
          nil

        {:ok, ""} ->
          nil

        {:ok, photo_url} ->
          transaction_repo.insert(
            ReviewPhoto.changeset(
              %ReviewPhoto{},
              %{review_id: review_id, photo_url: photo_url}
            )
          )

        :error ->
          nil
      end
    end)
    |> Repo.transaction()
 

This seems to work if the photo_url has a value, but if it’s nil or empty (someone hasn’t attached a picture) it fails with the following error:

[error] GenServer #PID<0.781.0> terminating
** (RuntimeError) expected Ecto.Multi callback named `:review_photo` to return either {:ok, value} or {:error, value}, got: nil
    (ecto 3.12.5) lib/ecto/multi.ex:905: Ecto.Multi.apply_operation/5

What am I missing here?

The issue occurs because Ecto.Multi.run callbacks must return {:ok, value} or {:error, value}, but your code is returning nil.

Try this:

Multi.new()
|> Multi.insert(:review, review_changeset)
|> Multi.run(:review_photo, fn transaction_repo, %{review: %Review{id: review_id}} ->
  case Map.fetch(attrs, "photo_url") do
    {:ok, nil} ->
      {:ok, nil} # Return {:ok, nil} instead of nil

    {:ok, ""} ->
      {:ok, nil} # Return {:ok, nil} instead of nil

    {:ok, photo_url} ->
      case transaction_repo.insert(
        ReviewPhoto.changeset(
          %ReviewPhoto{},
          %{review_id: review_id, photo_url: photo_url}
        )
      ) 

    :error ->
      {:error, nil} 
  end
end)
|> Repo.transaction()
1 Like