Skip to content

Commit 9aa8442

Browse files
committed
fix: handle nil values properly in atomic changing validations
1 parent bf31c46 commit 9aa8442

File tree

2 files changed

+56
-1
lines changed

2 files changed

+56
-1
lines changed

lib/ash/resource/validation/changing.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@ defmodule Ash.Resource.Validation.Changing do
8585
else
8686
case atomic_field(changeset, Keyword.fetch!(opts, :field)) do
8787
{:changing, field} ->
88-
{:atomic, [field], expr(^atomic_ref(field) == ^ref(field)),
88+
{:atomic, [field],
89+
expr(
90+
(is_nil(^atomic_ref(field)) and is_nil(^ref(field))) or
91+
(not is_nil(^ref(field)) and ^atomic_ref(field) == ^ref(field))
92+
),
8993
expr(
9094
error(^InvalidAttribute, %{
9195
field: ^field,

test/resource/validation/changing_test.exs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,5 +161,56 @@ defmodule Ash.Test.Resource.Validation.ChangingTest do
161161
|> Ash.bulk_update!(:ensure_comments_changing, %{}, return_errors?: true)
162162
end
163163
end
164+
165+
test "succeeds when changing from nil to a value" do
166+
# Create post with order = nil
167+
post = Ash.create!(Post, %{title: "foo"})
168+
assert is_nil(post.order)
169+
170+
# Should succeed when changing from nil to a value
171+
post
172+
|> assert_valid_updates(:ensure_order_changing, %{order: 42})
173+
end
174+
175+
test "absent validation with changing condition fails when setting nil field" do
176+
# This reproduces the exact Eden issue: absent(:outcome) with where: [changing(:outcome)]
177+
# should prevent setting a value on a field that was nil
178+
179+
defmodule TestPost do
180+
use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets
181+
182+
actions do
183+
default_accept :*
184+
defaults [:read, create: :*, update: :*]
185+
186+
update :close_task do
187+
# This should fail when trying to set outcome on a task that had no outcome
188+
validate absent(:outcome) do
189+
where [changing(:outcome)]
190+
message "Task already closed"
191+
end
192+
end
193+
end
194+
195+
attributes do
196+
uuid_primary_key :id
197+
attribute :title, :string, public?: true
198+
attribute :outcome, :string, public?: true
199+
end
200+
end
201+
202+
# Create a post with outcome = nil
203+
post = Ash.create!(TestPost, %{title: "test"})
204+
assert is_nil(post.outcome)
205+
206+
# Try to update outcome from nil to "closed" - this should fail
207+
# because absent(:outcome) with where: [changing(:outcome)] should prevent
208+
# setting a value when the field was previously absent (nil)
209+
assert_raise Ash.Error.Invalid, ~r/Task already closed/, fn ->
210+
TestPost
211+
|> Ash.Query.filter(id == ^post.id)
212+
|> Ash.bulk_update!(:close_task, %{outcome: "closed"})
213+
end
214+
end
164215
end
165216
end

0 commit comments

Comments
 (0)