Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions lib/ecto/repo/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ defmodule Ecto.Repo.Schema do
# changeset as changes, except the primary key if it is nil.
changeset = put_repo_and_action(changeset, :insert, repo, tuplet)
changeset = Relation.surface_changes(changeset, struct, keep_fields ++ drop_fields ++ assocs)
changeset = update_in(changeset.changes, &drop_non_writable_changes!(&1, drop_fields, schema, :insert))
changeset = process_non_writable_fields!(changeset, drop_fields, schema, :insert)

wrap_in_transaction(adapter, adapter_meta, opts, changeset, assocs, embeds, prepare, fn ->
assoc_opts = assoc_opts(assocs, opts)
Expand Down Expand Up @@ -561,7 +561,7 @@ defmodule Ecto.Repo.Schema do
# fields into the changeset. All changes must be in the
# changeset before hand.
changeset = put_repo_and_action(changeset, :update, repo, tuplet)
changeset = update_in(changeset.changes, &drop_non_writable_changes!(&1, drop_fields, schema, :update))
changeset = process_non_writable_fields!(changeset, drop_fields, schema, :update)

if changeset.changes != %{} or force? do
wrap_in_transaction(adapter, adapter_meta, opts, changeset, assocs, embeds, prepare, fn ->
Expand Down Expand Up @@ -625,16 +625,31 @@ defmodule Ecto.Repo.Schema do
{:error, put_repo_and_action(changeset, :update, repo, tuplet)}
end

defp drop_non_writable_changes!(changes, non_writable_fields, schema, action) do
Enum.reduce(non_writable_fields, changes, fn field, changes ->
case changes do
%{^field => _change} ->
handle_writable_violation(field, schema, action)
changes
%{} ->
changes
end
end)
defp process_non_writable_fields!(changeset, non_writable_fields, schema, action) do
changes =
Enum.reduce(non_writable_fields, changeset.changes, fn field, changes ->
case changes do
%{^field => _change} ->
handle_writable_violation(field, schema, action)
Map.delete(changes, field)
%{} ->
changes
end
end)

# On insert, if the underlying struct has a value for a non-writable
# field, we need to nilify it so that the struct returned from the
# insert operation reflects that the write was not actually performed.
# Discarding the change above only prevents the actual write from happening.
data = case action do
:insert ->
updates = Enum.map(non_writable_fields, fn field -> {field, nil} end)
struct!(changeset.data, updates)
_ ->
changeset.data
end
Comment thread
greg-rychlewski marked this conversation as resolved.
Outdated

%{changeset | data: data, changes: changes}
end

defp handle_writable_violation(field, schema, action) do
Expand Down
3 changes: 1 addition & 2 deletions lib/ecto/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -706,8 +706,7 @@ defmodule Ecto.Schema do
attempts to modify a field that should not be modified according to it's `:writable` value.
Must be one of `:nothing`, `:warn`, or `:raise`. If set to `:nothing`, the modification is
silently ignored. If set to `:warn`, the modification is ignored and a warning is logged. If set
to `:raise`, an exception is raised and the operation is aborted. If `:writable` is set to `:always`,
`:on_writable_violation` must be set to `:nothing`. Defaults to `:nothing`.
to `:raise`, an exception is raised and the operation is aborted. Defaults to `:nothing`.

"""
defmacro field(name, type \\ :string, opts \\ []) do
Expand Down
73 changes: 41 additions & 32 deletions test/ecto/repo_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2440,18 +2440,20 @@ defmodule Ecto.RepoTest do
end

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
test "update with on_writable_violation: :nothing saves changes for writable: :always and ignores changes for writable: :insert/:never" do
%{never: nil} =
%MySchemaWritable{id: 1}
|> Ecto.Changeset.change(%{always: 10, never: 11, insert: 12})
|> TestRepo.update!()
assert_received {:update, %{changes: [always: 10]}}
end

I think this is enough and then you can also make this minimal change to the existing insert test.


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

assert_received {:update, %{changes: [always: 10]}}
end)
Expand All @@ -2464,18 +2466,19 @@ defmodule Ecto.RepoTest do
assert_raise ArgumentError, "attempted to write to non-writable field :never during update", fn ->
%MySchemaWritableRaise{id: 1}
|> Ecto.Changeset.change(%{never: 10})
|> TestRepo.update()
|> TestRepo.update!()
end

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

%MySchemaWritableRaise{id: 3}
|> Ecto.Changeset.change(%{always: 12})
|> TestRepo.update()
%{always: 12, never: nil, insert: nil} =
%MySchemaWritableRaise{id: 3}
|> Ecto.Changeset.change(%{always: 12})
|> TestRepo.update!()
Comment thread
greg-rychlewski marked this conversation as resolved.
Outdated

assert_received {:update, %{changes: [always: 12]}}
end
Expand Down Expand Up @@ -2514,28 +2517,31 @@ defmodule Ecto.RepoTest do
end

test "insert with surfaced changes on_writable_violation: :nothing saves changes for writable: :always/:insert and ignores changes for writable: :never" do
%MySchemaWritable{id: 1, always: 10, never: 11, insert: 12}
|> Ecto.Changeset.change(%{})
|> TestRepo.insert()
%{always: 10, never: nil, insert: 12} =
%MySchemaWritable{id: 1, always: 10, never: 11, insert: 12}
|> Ecto.Changeset.change(%{})
|> TestRepo.insert!()

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

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

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

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
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
log = capture_log(fn ->
%MySchemaWritableWarn{id: 1, always: 10, never: 11, insert: 12}
|> Ecto.Changeset.change(%{})
|> TestRepo.insert()
%{always: 10, never: nil, insert: 12} =
%MySchemaWritableWarn{id: 1, always: 10, never: 11, insert: 12}
|> Ecto.Changeset.change(%{})
|> TestRepo.insert!()

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

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

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

%MySchemaWritableRaise{id: 2, insert: 11, always: 12}
|> Ecto.Changeset.change(%{})
|> TestRepo.insert()
%{always: 12, never: nil, insert: 11} =
%MySchemaWritableRaise{id: 2, insert: 11, always: 12}
|> Ecto.Changeset.change(%{})
|> TestRepo.insert!()

assert_received {:insert, %{fields: inserted_fields}}
assert Enum.sort(inserted_fields) == [always: 12, id: 2, insert: 11]
Expand All @@ -2576,12 +2584,13 @@ defmodule Ecto.RepoTest do
assert_raise ArgumentError, "attempted to write to non-writable field :never during insert", fn ->
%MySchemaWritableRaise{id: 1}
|> Ecto.Changeset.change(%{never: 10})
|> TestRepo.insert()
|> TestRepo.insert!()
end

%MySchemaWritableRaise{id: 2}
|> Ecto.Changeset.change(%{insert: 11, always: 12})
|> TestRepo.insert()
%{always: 12, never: nil, insert: 11} =
Comment thread
greg-rychlewski marked this conversation as resolved.
Outdated
%MySchemaWritableRaise{id: 2}
|> Ecto.Changeset.change(%{insert: 11, always: 12})
|> TestRepo.insert!()

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