Skip to content

Commit 2e59540

Browse files
authored
Do not reflect changes ignored due to :writable in returned struct (#4736)
1 parent 7cb27b3 commit 2e59540

3 files changed

Lines changed: 71 additions & 34 deletions

File tree

lib/ecto/repo/schema.ex

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -452,8 +452,9 @@ defmodule Ecto.Repo.Schema do
452452
# On insert, we always merge the whole struct into the
453453
# changeset as changes, except the primary key if it is nil.
454454
changeset = put_repo_and_action(changeset, :insert, repo, tuplet)
455-
changeset = Relation.surface_changes(changeset, struct, keep_fields ++ drop_fields ++ assocs)
456-
changeset = update_in(changeset.changes, &drop_non_writable_changes!(&1, drop_fields, schema, :insert))
455+
changeset = Relation.surface_changes(changeset, struct, keep_fields ++ assocs)
456+
changeset = detect_unsurfaced_non_writable_data!(changeset, drop_fields, schema)
457+
changeset = drop_non_writable_changes!(changeset, drop_fields, schema, :insert)
457458

458459
wrap_in_transaction(adapter, adapter_meta, opts, changeset, assocs, embeds, prepare, fn ->
459460
assoc_opts = assoc_opts(assocs, opts)
@@ -522,6 +523,23 @@ defmodule Ecto.Repo.Schema do
522523
{:error, put_repo_and_action(changeset, :insert, repo, tuplet)}
523524
end
524525

526+
defp detect_unsurfaced_non_writable_data!(changeset, [], _schema) do
527+
changeset
528+
end
529+
530+
defp detect_unsurfaced_non_writable_data!(changeset, non_writable_fields, schema) do
531+
Enum.each(non_writable_fields, fn field ->
532+
case changeset.data do
533+
%{^field => value} when value != nil ->
534+
handle_writable_violation(field, schema, :insert)
535+
%{} ->
536+
:ok
537+
end
538+
end)
539+
540+
changeset
541+
end
542+
525543
@doc """
526544
Implementation for `Ecto.Repo.update/2`.
527545
"""
@@ -561,7 +579,7 @@ defmodule Ecto.Repo.Schema do
561579
# fields into the changeset. All changes must be in the
562580
# changeset before hand.
563581
changeset = put_repo_and_action(changeset, :update, repo, tuplet)
564-
changeset = update_in(changeset.changes, &drop_non_writable_changes!(&1, drop_fields, schema, :update))
582+
changeset = drop_non_writable_changes!(changeset, drop_fields, schema, :update)
565583

566584
if changeset.changes != %{} or force? do
567585
wrap_in_transaction(adapter, adapter_meta, opts, changeset, assocs, embeds, prepare, fn ->
@@ -625,16 +643,23 @@ defmodule Ecto.Repo.Schema do
625643
{:error, put_repo_and_action(changeset, :update, repo, tuplet)}
626644
end
627645

628-
defp drop_non_writable_changes!(changes, non_writable_fields, schema, action) do
629-
Enum.reduce(non_writable_fields, changes, fn field, changes ->
646+
defp drop_non_writable_changes!(changeset, [], _schema, _action) do
647+
changeset
648+
end
649+
650+
defp drop_non_writable_changes!(changeset, non_writable_fields, schema, action) do
651+
changes = Enum.reduce(non_writable_fields, changeset.changes, fn field, changes ->
630652
case changes do
631653
%{^field => _change} ->
632654
handle_writable_violation(field, schema, action)
633-
changes
655+
656+
Map.delete(changes, field)
634657
%{} ->
635658
changes
636659
end
637660
end)
661+
662+
%{changeset | changes: changes}
638663
end
639664

640665
defp handle_writable_violation(field, schema, action) do

lib/ecto/schema.ex

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -710,8 +710,7 @@ defmodule Ecto.Schema do
710710
attempts to modify a field that should not be modified according to it's `:writable` value.
711711
Must be one of `:nothing`, `:warn`, or `:raise`. If set to `:nothing`, the modification is
712712
silently ignored. If set to `:warn`, the modification is ignored and a warning is logged. If set
713-
to `:raise`, an exception is raised and the operation is aborted. If `:writable` is set to `:always`,
714-
`:on_writable_violation` must be set to `:nothing`. Defaults to `:nothing`.
713+
to `:raise`, an exception is raised and the operation is aborted. Defaults to `:nothing`.
715714
716715
"""
717716
defmacro field(name, type \\ :string, opts \\ []) do

test/ecto/repo_test.exs

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2440,18 +2440,20 @@ defmodule Ecto.RepoTest do
24402440
end
24412441

24422442
test "update with on_writable_violation: :nothing saves changes for writable: :always and ignores changes for writable: :insert/:never" do
2443-
%MySchemaWritable{id: 1}
2444-
|> Ecto.Changeset.change(%{always: 10, never: 11, insert: 12})
2445-
|> TestRepo.update()
2443+
%{always: 10, never: nil, insert: nil} =
2444+
%MySchemaWritable{id: 1}
2445+
|> Ecto.Changeset.change(%{always: 10, never: 11, insert: 12})
2446+
|> TestRepo.update!()
24462447

24472448
assert_received {:update, %{changes: [always: 10]}}
24482449
end
24492450

24502451
test "update with on_writable_violation: :warn saves changes for writable: :always, ignores changes for writable: :insert/:never, and logs a warning" do
24512452
log = capture_log(fn ->
2452-
%MySchemaWritableWarn{id: 1}
2453-
|> Ecto.Changeset.change(%{always: 10, never: 11, insert: 12})
2454-
|> TestRepo.update()
2453+
%{always: 10, never: nil, insert: nil} =
2454+
%MySchemaWritableWarn{id: 1}
2455+
|> Ecto.Changeset.change(%{always: 10, never: 11, insert: 12})
2456+
|> TestRepo.update!()
24552457

24562458
assert_received {:update, %{changes: [always: 10]}}
24572459
end)
@@ -2464,18 +2466,18 @@ defmodule Ecto.RepoTest do
24642466
assert_raise ArgumentError, "attempted to write to non-writable field :never during update", fn ->
24652467
%MySchemaWritableRaise{id: 1}
24662468
|> Ecto.Changeset.change(%{never: 10})
2467-
|> TestRepo.update()
2469+
|> TestRepo.update!()
24682470
end
24692471

24702472
assert_raise ArgumentError, "attempted to write to non-writable field :insert during update", fn ->
24712473
%MySchemaWritableRaise{id: 2}
24722474
|> Ecto.Changeset.change(%{insert: 11})
2473-
|> TestRepo.update()
2475+
|> TestRepo.update!()
24742476
end
24752477

24762478
%MySchemaWritableRaise{id: 3}
24772479
|> Ecto.Changeset.change(%{always: 12})
2478-
|> TestRepo.update()
2480+
|> TestRepo.update!()
24792481

24802482
assert_received {:update, %{changes: [always: 12]}}
24812483
end
@@ -2514,28 +2516,37 @@ defmodule Ecto.RepoTest do
25142516
end
25152517

25162518
test "insert with surfaced changes on_writable_violation: :nothing saves changes for writable: :always/:insert and ignores changes for writable: :never" do
2517-
%MySchemaWritable{id: 1, always: 10, never: 11, insert: 12}
2518-
|> Ecto.Changeset.change(%{})
2519-
|> TestRepo.insert()
2519+
# For surfaced changes from the underlying struct, the value in the returned struct is
2520+
# maintained even though the underlying write was not performed, as opposed to "normal" changes
2521+
# provided via a changeset.
2522+
%{always: 10, never: 11, insert: 12} =
2523+
%MySchemaWritable{id: 1, always: 10, never: 11, insert: 12}
2524+
|> Ecto.Changeset.change(%{})
2525+
|> TestRepo.insert!()
25202526

25212527
assert_received {:insert, %{fields: inserted_fields}}
25222528
assert Enum.sort(inserted_fields) == [always: 10, id: 1, insert: 12]
25232529
end
25242530

25252531
test "insert with on_writable_violation: :nothing saves changes for writable: :always/:insert and ignores changes for writable: :never" do
2526-
%MySchemaWritable{id: 1}
2527-
|> Ecto.Changeset.change(%{always: 10, never: 11, insert: 12})
2528-
|> TestRepo.insert()
2532+
%{always: 10, never: nil, insert: 12} =
2533+
%MySchemaWritable{id: 1}
2534+
|> Ecto.Changeset.change(%{always: 10, never: 11, insert: 12})
2535+
|> TestRepo.insert!()
25292536

25302537
assert_received {:insert, %{fields: inserted_fields}}
25312538
assert Enum.sort(inserted_fields) == [always: 10, id: 1, insert: 12]
25322539
end
25332540

2534-
test "insert with with surfaced changes and on_writable_violation: :warn saves changes for writable: :always/:insert, ignores changes for writable: :never, and logs a warning" do
2541+
test "insert with surfaced changes and on_writable_violation: :warn saves changes for writable: :always/:insert, ignores changes for writable: :never, and logs a warning" do
25352542
log = capture_log(fn ->
2536-
%MySchemaWritableWarn{id: 1, always: 10, never: 11, insert: 12}
2537-
|> Ecto.Changeset.change(%{})
2538-
|> TestRepo.insert()
2543+
# For surfaced changes from the underlying struct, the value in the returned struct is
2544+
# maintained even though the underlying write was not performed, as opposed to "normal" changes
2545+
# provided via a changeset.
2546+
%{always: 10, never: 11, insert: 12} =
2547+
%MySchemaWritableWarn{id: 1, always: 10, never: 11, insert: 12}
2548+
|> Ecto.Changeset.change(%{})
2549+
|> TestRepo.insert!()
25392550

25402551
assert_received {:insert, %{fields: inserted_fields}}
25412552
assert Enum.sort(inserted_fields) == [always: 10, id: 1, insert: 12]
@@ -2546,9 +2557,10 @@ defmodule Ecto.RepoTest do
25462557

25472558
test "insert with on_writable_violation: :warn saves changes for writable: :always/:insert, ignores changes for writable: :never, and logs a warning" do
25482559
log = capture_log(fn ->
2549-
%MySchemaWritableWarn{id: 1}
2550-
|> Ecto.Changeset.change(%{always: 10, never: 11, insert: 12})
2551-
|> TestRepo.insert()
2560+
%{always: 10, never: nil, insert: 12} =
2561+
%MySchemaWritableWarn{id: 1}
2562+
|> Ecto.Changeset.change(%{always: 10, never: 11, insert: 12})
2563+
|> TestRepo.insert!()
25522564

25532565
assert_received {:insert, %{fields: inserted_fields}}
25542566
assert Enum.sort(inserted_fields) == [always: 10, id: 1, insert: 12]
@@ -2561,12 +2573,13 @@ defmodule Ecto.RepoTest do
25612573
assert_raise ArgumentError, "attempted to write to non-writable field :never during insert", fn ->
25622574
%MySchemaWritableRaise{id: 1, never: 10}
25632575
|> Ecto.Changeset.change(%{})
2564-
|> TestRepo.insert()
2576+
|> TestRepo.insert!()
25652577
end
25662578

2579+
25672580
%MySchemaWritableRaise{id: 2, insert: 11, always: 12}
25682581
|> Ecto.Changeset.change(%{})
2569-
|> TestRepo.insert()
2582+
|> TestRepo.insert!()
25702583

25712584
assert_received {:insert, %{fields: inserted_fields}}
25722585
assert Enum.sort(inserted_fields) == [always: 12, id: 2, insert: 11]
@@ -2576,12 +2589,12 @@ defmodule Ecto.RepoTest do
25762589
assert_raise ArgumentError, "attempted to write to non-writable field :never during insert", fn ->
25772590
%MySchemaWritableRaise{id: 1}
25782591
|> Ecto.Changeset.change(%{never: 10})
2579-
|> TestRepo.insert()
2592+
|> TestRepo.insert!()
25802593
end
25812594

25822595
%MySchemaWritableRaise{id: 2}
25832596
|> Ecto.Changeset.change(%{insert: 11, always: 12})
2584-
|> TestRepo.insert()
2597+
|> TestRepo.insert!()
25852598

25862599
assert_received {:insert, %{fields: inserted_fields}}
25872600
assert Enum.sort(inserted_fields) == [always: 12, id: 2, insert: 11]

0 commit comments

Comments
 (0)