diff --git a/lib/ecto/repo/schema.ex b/lib/ecto/repo/schema.ex index 730e50a933..4d32d2f95b 100644 --- a/lib/ecto/repo/schema.ex +++ b/lib/ecto/repo/schema.ex @@ -458,7 +458,7 @@ defmodule Ecto.Repo.Schema do wrap_in_transaction(adapter, adapter_meta, opts, changeset, assocs, embeds, prepare, fn -> assoc_opts = assoc_opts(assocs, opts) - user_changeset = run_prepare(changeset, prepare) + user_changeset = changeset |> run_prepare(prepare) |> drop_non_writable_changes!(drop_fields, schema, :insert) {changeset, parents, children, _} = pop_assocs(user_changeset, assocs) changeset = process_parents(changeset, user_changeset, parents, [], adapter, assoc_opts) @@ -584,7 +584,7 @@ defmodule Ecto.Repo.Schema do if changeset.changes != %{} or force? do wrap_in_transaction(adapter, adapter_meta, opts, changeset, assocs, embeds, prepare, fn -> assoc_opts = assoc_opts(assocs, opts) - user_changeset = run_prepare(changeset, prepare) + user_changeset = changeset |> run_prepare(prepare) |> drop_non_writable_changes!(drop_fields, schema, :update) {changeset, parents, children, reset_parents} = pop_assocs(user_changeset, assocs) diff --git a/test/ecto/repo_test.exs b/test/ecto/repo_test.exs index 5d9f34587b..97c01bfe4b 100644 --- a/test/ecto/repo_test.exs +++ b/test/ecto/repo_test.exs @@ -2521,6 +2521,28 @@ defmodule Ecto.RepoTest do assert_received {:update, %{changes: [always: 12]}} end + test "update enforces writable fields added by prepare_changes" do + %{always: 10, never: nil} = + %MySchemaWritable{id: 1} + |> Ecto.Changeset.change(%{always: 10}) + |> Ecto.Changeset.prepare_changes(&Ecto.Changeset.put_change(&1, :never, 11)) + |> TestRepo.update!() + + assert_received {:update, %{changes: [always: 10]}} + + message = ~r""" + you are attempting to write to the field :never of #{inspect(__MODULE__.MySchemaWritableRaise)} but + the `:writable` option of this field indicates the field should not be written to during an update. + """ + + assert_raise ArgumentError, message, fn -> + %MySchemaWritableRaise{id: 2} + |> Ecto.Changeset.change(%{always: 12}) + |> Ecto.Changeset.prepare_changes(&Ecto.Changeset.put_change(&1, :never, 13)) + |> TestRepo.update!() + end + end + test "update is a no-op when updatable fields are not changed" do %MySchemaWritable{id: 1} |> Ecto.Changeset.change(%{never: "can't update", insert: "can't update either"}) @@ -2654,6 +2676,29 @@ defmodule Ecto.RepoTest do assert Enum.sort(inserted_fields) == [always: 12, id: 2, insert: 11] end + test "insert enforces writable fields added by prepare_changes" do + %{always: 10, never: nil} = + %MySchemaWritable{id: 1} + |> Ecto.Changeset.change(%{always: 10}) + |> Ecto.Changeset.prepare_changes(&Ecto.Changeset.put_change(&1, :never, 11)) + |> TestRepo.insert!() + + assert_received {:insert, %{fields: inserted_fields}} + assert Enum.sort(inserted_fields) == [always: 10, id: 1] + + message = ~r""" + you are attempting to write to the field :never of #{inspect(__MODULE__.MySchemaWritableRaise)} but + the `:writable` option of this field indicates the field should not be written to during an insert. + """ + + assert_raise ArgumentError, message, fn -> + %MySchemaWritableRaise{id: 2} + |> Ecto.Changeset.change(%{always: 12}) + |> Ecto.Changeset.prepare_changes(&Ecto.Changeset.put_change(&1, :never, 13)) + |> TestRepo.insert!() + end + end + test "insert with returning" do %MySchemaWritable{id: 1} |> Ecto.Changeset.change(%{always: 10, never: 11, insert: 12})