Ecto.New broken after recent update

I recently updated Ecto along with some other packages and noticed some function changes. They added Repo.transact\2, so I could remove my helper function which I found on these forums a while back. Nice :slight_smile:

But some other code was now showing warnings - anything starting with Ecto.Multi.new

Here’s the shortened warning:

The call 'Elixir.Ecto.Multi':update
         (#{'__struct__' := 'Elixir.Ecto.Multi',
            'names' := #{'__struct__' := 'Elixir.MapSet', 'map' := #{}},
            'operations' := []},
          'user',
          _changeset@1 ::
            ...,
                'validations' := [{atom(), _}]}) does not have a term of type 
          #{'__struct__' := 'Elixir.Ecto.Multi',
            'names' :=
                #{'__struct__' := 'Elixir.MapSet',
                  'map' := 'Elixir.MapSet':internal(_)},
            'operations' := [{_, {_, _} | {_, _, _} | {_, _, _, _}}]} (with opaque subterms) as 1st argument

Here’s the code:

Ecto.Multi.new()
    |> Ecto.Multi.update(:user, changeset)
    |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
    |> Repo.transact()
    |> case do
      {:ok, %{user: user}} -> {:ok, user}
      {:error, :user, changeset, _} -> {:error, changeset}
    end

It pops up in 6 other places, also with complaints about insert_all.

So I’m sure the problem is with Multi.new but the changelog never mentions it.

Any idea what is going on here?

1 Like

I wonder if it’s somehow related to the MapSet default used in Ecto.Multi.new being in the struct definition:

Does the Dialyzer error change if you replace Ecto.Multi.new with an explicit initialization?

%Ecto.Multi{names: MapSet.new(), operations: []}
    |> Ecto.Multi.update(:user, changeset)
    |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
...

I suspect so—per the warning

#{
  '__struct__' := 'Elixir.Ecto.Multi',
  'names' := #{
    '__struct__' := 'Elixir.MapSet',
-    'map' := #{}
+    'map' := 'Elixir.MapSet':internal(_)}
  },
  'operations' := []
}

The :map field of the MapSet struct was made @opaque in Elixir 3 years ago:

I’m guessing somebody’s code or typespec (possibly yours or Ecto’s) is relying on the internal fields of the MapSet struct, including expecting the now-opaque :map field to be a map. (MapSet uses :sets under the hood, whose typing is also opaque, so there’s not much to do here except not rely on such fields.)

Unfortunately the warning is still there after this change.

Some of the functions where generated with the default Phoenix gen.auth but I started this project with This book, so sure they are probably old now :smiley: but I have other parts which are more recent with the same problem.

That said, I cleaned and recompiled all deps so I don’t think its from old code anymore. These were the only changes in the mix file… unless Dialyzer updated and I forgot?

  {:ecto_sql, "~> 3.12.1"} -> {:ecto_sql, "~> 3.13.2"},
  {:floki, ">= 0.37.1", only: :test} -> {:floki, ">= 0.38.0", only: :test},
  {:phoenix_ecto, "~> 4.6.4"} -> {:phoenix_ecto, "~> 4.6.5"},
  {:req, "~> 0.5.10"} -> {:req, "~> 0.5.12"},
  {:swoosh, "~> 1.19.1"} -> {:swoosh, "~> 1.19.3"},
  {:timex, "~> 3.7.11"} -> {:timex, "~> 3.7.13"}