ecto icon indicating copy to clipboard operation
ecto copied to clipboard

Fix `Changeset.changed?/2` for assocs

Open jesenko opened this issue 1 year ago • 6 comments

It now returns true whenever fetch_change? != :error. Previously, it returned false when assoc was set to existing struct (as changeset.changes == %{}) and errored when set to nil (due to nil.changes != %{} check in relation_changed?.

Fixes #4543.

jesenko avatar Nov 05 '24 11:11 jesenko

We should fix it to handle nil because I think this code is unfortunately wrong. We could have an association that has not changed (i.e. it is an empty changeset).

josevalim avatar Nov 06 '24 12:11 josevalim

Hi @josevalim thank you for looking into this. Note that handling nil is not the only issue — there is a valid case where action==:update && changes==%{}, e.g. when setting association to existing struct as in https://github.com/elixir-ecto/ecto/pull/4544/files#diff-8176128515609669c40d0d061249534c49e369abd7bc42c61cdbf5c148da388dR590

jesenko avatar Nov 06 '24 13:11 jesenko

Yes, we need to check more cases. And potentialy make it recursive. But simply checking if the field is set is not enough.

josevalim avatar Nov 06 '24 13:11 josevalim

Ping! :)

josevalim avatar Dec 23 '24 20:12 josevalim

Sorry for the delay. I've been tied up with other priorities — I will try to move this forward till the end of this month...

jesenko avatar Jan 18 '25 07:01 jesenko

@josevalim I am having some difficulty coming up with test cases that would fail with Changeset.changed? behaviour introduced in this PR (compared to current behaviour); it seems that Changeset functions for assoc/embed manipulation already prune most of empty assoc changesets, e.g.

{:ok, existing_profile} = TestRepo.insert(%Profile{name: "profile"})
%Author{profile: existing_profile}
      |> Changeset.change()
      |> Changeset.put_assoc(:profile, existing_profile |> Changeset.change(name: "aa"))
      |> Changeset.put_assoc(:profile, existing_profile |> Changeset.change(name: "profile"))

already removes profile assoc changeset (as it would contain changes: %{}) from author changeset profile. Same goes for cast_assoc, and after quick inspection, I think also for has_many and has_one associations.

We could probably manipulate changeset struct directly, but we should probably test for scenarios that can results from using Changeset functions?

jesenko avatar Jan 26 '25 22:01 jesenko