Repository: elixir-ecto/ecto
Pull request #4278 (2023-09-15 08:30:11)

Surface embed on insert
Inserting data from a changeset pushes any unchanged fields from `data` into `changes`. 

Yesterday I was reviewing some code and noticed the same thing doesn't happen for embedded fields. Instead of surfacing the `data` from an embedded field into its `changes`, it is marked as an update and raises.

I saw similar behaviour for deletes was [reversed here](https://github.com/elixir-ecto/ecto/commit/d65b745bdb08cac7f92726f4b258db3770f1d509) so thought it might be desirable for updates as well. It seems to make sense to me to do so but I might be missing some important context. 


Patch:
diff --git a/lib/ecto/embedded.ex b/lib/ecto/embedded.ex
index 7f0099f7..da42ad80 100644
--- a/lib/ecto/embedded.ex
+++ b/lib/ecto/embedded.ex
@@ -168,14 +168,14 @@ defmodule Ecto.Embedded do
   end
 
   defp prepare_each(%{cardinality: :one} = embed, changeset, adapter, repo, repo_action) do
-    action = check_action!(changeset.action, repo_action, embed)
+    action = normalize_action(changeset.action, repo_action, embed)
     changeset = run_prepare(changeset, repo)
     to_struct(changeset, action, embed, adapter)
   end
 
   defp prepare_each(%{cardinality: :many} = embed, changesets, adapter, repo, repo_action) do
     for changeset <- changesets,
-        action = check_action!(changeset.action, repo_action, embed),
+        action = normalize_action(changeset.action, repo_action, embed),
         changeset = run_prepare(changeset, repo),
         prepared = to_struct(changeset, action, embed, adapter),
         do: prepared
@@ -244,15 +244,9 @@ defmodule Ecto.Embedded do
     struct(struct, changes)
   end
 
-  defp check_action!(:replace, action, %{on_replace: :delete} = embed),
-    do: check_action!(:delete, action, embed)
-
-  defp check_action!(:update, :insert, %{related: schema}) do
-    raise ArgumentError,
-          "got action :update in changeset for embedded #{inspect(schema)} while inserting"
-  end
-
-  defp check_action!(action, _, _), do: action
+  defp normalize_action(:replace, _, %{on_replace: :delete}), do: :delete
+  defp normalize_action(:update, :insert, _), do: :insert
+  defp normalize_action(action, _, _), do: action
 
   defp autogenerate_id(changes, _struct, :insert, schema, adapter) do
     case schema.__schema__(:autogenerate_id) do


Test patch:
diff --git a/test/ecto/repo_test.exs b/test/ecto/repo_test.exs
index aa5c5528..a5cc9721 100644
--- a/test/ecto/repo_test.exs
+++ b/test/ecto/repo_test.exs
@@ -36,6 +36,11 @@ defmodule Ecto.RepoTest do
 
     embedded_schema do
       field :x, :string
+      field :y, :string
+    end
+
+    def changeset(struct, params) do
+      Ecto.Changeset.cast(struct, params, [:x])
     end
   end
 
@@ -131,7 +136,7 @@ defmodule Ecto.RepoTest do
 
     schema "my_schema" do
       field :x, :string
-      embeds_many :embeds, MyEmbed
+      embeds_many :embeds, MyEmbed, on_replace: :delete
     end
   end
 
@@ -1196,6 +1201,30 @@ defmodule Ecto.RepoTest do
         TestRepo.insert_or_update %MySchema{}
       end
     end
+
+    test "insert surfaces embed fields" do
+      # embeds_one
+      inserted =
+        %MySchemaEmbedsOne{embed: %MyEmbed{x: "old_x", y: "old_y"}}
+        |> Ecto.Changeset.cast(%{embed: %{x: "new_x"}}, [])
+        |> Ecto.Changeset.cast_embed(:embed)
+        |> TestRepo.insert!()
+
+      assert %{x: "new_x", y: "old_y"} = inserted.embed
+
+      # embeds_many
+      data_embed1 = %MyEmbed{id: 1, x: "old_x_1", y: "old_y_1"}
+      data_embed2 = %MyEmbed{id: 2, x: "old_x_2"}
+
+      inserted =
+        %MySchemaEmbedsMany{embeds: [data_embed1, data_embed2]}
+        |> Ecto.Changeset.cast(%{embeds: [%{id: 1, x: "new_x_1"}, %{}]}, [])
+        |> Ecto.Changeset.cast_embed(:embeds)
+        |> TestRepo.insert!()
+
+      assert [%{id: 1, x: "new_x_1", y: "old_y_1"}, %{id: new_id, x: nil}] = inserted.embeds
+      assert new_id != data_embed2.id
+    end
   end
 
   test "get, get_by, one and all sets schema prefix" do