Skip to content

Commit 3fff2a3

Browse files
authored
Enforce writable fields after prepare_changes (#4771)
1 parent e2b35d2 commit 3fff2a3

2 files changed

Lines changed: 47 additions & 2 deletions

File tree

lib/ecto/repo/schema.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ defmodule Ecto.Repo.Schema do
458458

459459
wrap_in_transaction(adapter, adapter_meta, opts, changeset, assocs, embeds, prepare, fn ->
460460
assoc_opts = assoc_opts(assocs, opts)
461-
user_changeset = run_prepare(changeset, prepare)
461+
user_changeset = changeset |> run_prepare(prepare) |> drop_non_writable_changes!(drop_fields, schema, :insert)
462462

463463
{changeset, parents, children, _} = pop_assocs(user_changeset, assocs)
464464
changeset = process_parents(changeset, user_changeset, parents, [], adapter, assoc_opts)
@@ -584,7 +584,7 @@ defmodule Ecto.Repo.Schema do
584584
if changeset.changes != %{} or force? do
585585
wrap_in_transaction(adapter, adapter_meta, opts, changeset, assocs, embeds, prepare, fn ->
586586
assoc_opts = assoc_opts(assocs, opts)
587-
user_changeset = run_prepare(changeset, prepare)
587+
user_changeset = changeset |> run_prepare(prepare) |> drop_non_writable_changes!(drop_fields, schema, :update)
588588

589589
{changeset, parents, children, reset_parents} = pop_assocs(user_changeset, assocs)
590590

test/ecto/repo_test.exs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2521,6 +2521,28 @@ defmodule Ecto.RepoTest do
25212521
assert_received {:update, %{changes: [always: 12]}}
25222522
end
25232523

2524+
test "update enforces writable fields added by prepare_changes" do
2525+
%{always: 10, never: nil} =
2526+
%MySchemaWritable{id: 1}
2527+
|> Ecto.Changeset.change(%{always: 10})
2528+
|> Ecto.Changeset.prepare_changes(&Ecto.Changeset.put_change(&1, :never, 11))
2529+
|> TestRepo.update!()
2530+
2531+
assert_received {:update, %{changes: [always: 10]}}
2532+
2533+
message = ~r"""
2534+
you are attempting to write to the field :never of #{inspect(__MODULE__.MySchemaWritableRaise)} but
2535+
the `:writable` option of this field indicates the field should not be written to during an update.
2536+
"""
2537+
2538+
assert_raise ArgumentError, message, fn ->
2539+
%MySchemaWritableRaise{id: 2}
2540+
|> Ecto.Changeset.change(%{always: 12})
2541+
|> Ecto.Changeset.prepare_changes(&Ecto.Changeset.put_change(&1, :never, 13))
2542+
|> TestRepo.update!()
2543+
end
2544+
end
2545+
25242546
test "update is a no-op when updatable fields are not changed" do
25252547
%MySchemaWritable{id: 1}
25262548
|> Ecto.Changeset.change(%{never: "can't update", insert: "can't update either"})
@@ -2654,6 +2676,29 @@ defmodule Ecto.RepoTest do
26542676
assert Enum.sort(inserted_fields) == [always: 12, id: 2, insert: 11]
26552677
end
26562678

2679+
test "insert enforces writable fields added by prepare_changes" do
2680+
%{always: 10, never: nil} =
2681+
%MySchemaWritable{id: 1}
2682+
|> Ecto.Changeset.change(%{always: 10})
2683+
|> Ecto.Changeset.prepare_changes(&Ecto.Changeset.put_change(&1, :never, 11))
2684+
|> TestRepo.insert!()
2685+
2686+
assert_received {:insert, %{fields: inserted_fields}}
2687+
assert Enum.sort(inserted_fields) == [always: 10, id: 1]
2688+
2689+
message = ~r"""
2690+
you are attempting to write to the field :never of #{inspect(__MODULE__.MySchemaWritableRaise)} but
2691+
the `:writable` option of this field indicates the field should not be written to during an insert.
2692+
"""
2693+
2694+
assert_raise ArgumentError, message, fn ->
2695+
%MySchemaWritableRaise{id: 2}
2696+
|> Ecto.Changeset.change(%{always: 12})
2697+
|> Ecto.Changeset.prepare_changes(&Ecto.Changeset.put_change(&1, :never, 13))
2698+
|> TestRepo.insert!()
2699+
end
2700+
end
2701+
26572702
test "insert with returning" do
26582703
%MySchemaWritable{id: 1}
26592704
|> Ecto.Changeset.change(%{always: 10, never: 11, insert: 12})

0 commit comments

Comments
 (0)