2023-08-29 08:18:56 +12:00
|
|
|
defmodule AshPostgres.AtomicsTest do
|
2024-05-16 01:50:36 +12:00
|
|
|
alias AshPostgres.Test.Author
|
2024-07-23 03:49:59 +12:00
|
|
|
alias AshPostgres.Test.Comment
|
|
|
|
|
2023-08-29 08:18:56 +12:00
|
|
|
use AshPostgres.RepoCase, async: false
|
2024-07-28 06:27:02 +12:00
|
|
|
alias AshPostgres.Test.Invite
|
2024-03-28 09:52:28 +13:00
|
|
|
alias AshPostgres.Test.Post
|
2024-07-28 06:27:02 +12:00
|
|
|
alias AshPostgres.Test.User
|
2023-08-29 08:18:56 +12:00
|
|
|
|
|
|
|
import Ash.Expr
|
2024-05-16 03:15:34 +12:00
|
|
|
require Ash.Query
|
2023-08-29 08:18:56 +12:00
|
|
|
|
2023-10-12 08:17:50 +13:00
|
|
|
test "atomics work on upserts" do
|
|
|
|
id = Ash.UUID.generate()
|
|
|
|
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{id: id, title: "foo", price: 1}, upsert?: true)
|
|
|
|
|> Ash.Changeset.atomic_update(:price, expr(price + 1))
|
2024-03-28 09:52:28 +13:00
|
|
|
|> Ash.create!()
|
2023-10-12 08:17:50 +13:00
|
|
|
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{id: id, title: "foo", price: 1}, upsert?: true)
|
|
|
|
|> Ash.Changeset.atomic_update(:price, expr(price + 1))
|
2024-03-28 09:52:28 +13:00
|
|
|
|> Ash.create!()
|
2023-10-12 08:17:50 +13:00
|
|
|
|
2024-03-28 09:52:28 +13:00
|
|
|
assert [%{price: 2}] = Post |> Ash.read!()
|
2023-10-12 08:17:50 +13:00
|
|
|
end
|
|
|
|
|
2023-08-29 08:18:56 +12:00
|
|
|
test "a basic atomic works" do
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
2024-03-28 09:52:28 +13:00
|
|
|
|> Ash.create!()
|
2023-08-29 08:18:56 +12:00
|
|
|
|
|
|
|
assert %{price: 2} =
|
|
|
|
post
|
|
|
|
|> Ash.Changeset.for_update(:update, %{})
|
2023-09-01 03:47:15 +12:00
|
|
|
|> Ash.Changeset.atomic_update(:price, expr(price + 1))
|
2024-03-28 09:52:28 +13:00
|
|
|
|> Ash.update!()
|
2023-08-29 08:18:56 +12:00
|
|
|
end
|
|
|
|
|
2024-07-28 06:27:02 +12:00
|
|
|
test "a basic atomic works with enum/allow_nil? false" do
|
|
|
|
user =
|
|
|
|
User
|
|
|
|
|> Ash.Changeset.for_create(:create, %{name: "Dude", role: :user})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
Invite
|
|
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
|
|
name: "Dude",
|
|
|
|
role: :admin
|
|
|
|
})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
assert %{role: :admin} =
|
|
|
|
user
|
|
|
|
|> Ash.Changeset.for_update(:accept_invite, %{})
|
|
|
|
|> Ash.update!()
|
|
|
|
end
|
|
|
|
|
2024-05-23 09:47:10 +12:00
|
|
|
test "atomics work with maps that contain lists" do
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
assert %{list_of_stuff: [%{"foo" => [%{"a" => 1}]}]} =
|
|
|
|
post
|
|
|
|
|> Ash.Changeset.for_update(:update, %{list_of_stuff: [%{foo: [%{a: 1}]}]})
|
|
|
|
|> Ash.update!()
|
|
|
|
end
|
|
|
|
|
|
|
|
test "atomics work with maps that contain lists that contain maps that contain lists etc." do
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
assert %{list_of_stuff: [%{"foo" => [%{"a" => 1, "b" => %{"c" => [1, 2, 3]}}]}]} =
|
|
|
|
post
|
|
|
|
|> Ash.Changeset.for_update(:update, %{
|
|
|
|
list_of_stuff: [%{foo: [%{a: 1, b: %{c: [1, 2, 3]}}]}]
|
|
|
|
})
|
|
|
|
|> Ash.update!()
|
|
|
|
end
|
|
|
|
|
|
|
|
test "atomics work with maps that contain expressions in a deep structure" do
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
assert %{list_of_stuff: [%{"foo" => [%{"a" => 1, "b" => %{"c" => [1, 2, 3]}}]}]} =
|
|
|
|
post
|
|
|
|
|> Ash.Changeset.for_update(:update, %{})
|
|
|
|
|> Ash.Changeset.atomic_update(%{
|
|
|
|
list_of_stuff: [
|
|
|
|
%{foo: [%{a: 1, b: %{c: [1, 2, expr(type(fragment("3"), :integer))]}}]}
|
|
|
|
]
|
|
|
|
})
|
|
|
|
|> Ash.update!()
|
|
|
|
end
|
|
|
|
|
2024-05-30 04:29:34 +12:00
|
|
|
test "an atomic update can be set to the value of an aggregate" do
|
|
|
|
author =
|
|
|
|
Author
|
|
|
|
|> Ash.Changeset.for_create(:create, %{first_name: "John", last_name: "Doe"})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{title: "bar", author_id: author.id})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
# just asserting that there is no exception here
|
|
|
|
post
|
|
|
|
|> Ash.Changeset.for_update(:set_title_to_sum_of_author_count_of_posts)
|
|
|
|
|> Ash.update!()
|
|
|
|
end
|
|
|
|
|
2024-05-30 17:27:20 +12:00
|
|
|
test "an atomic update can be set to the value of a related aggregate" do
|
|
|
|
author =
|
|
|
|
Author
|
|
|
|
|> Ash.Changeset.for_create(:create, %{first_name: "John", last_name: "Doe"})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{title: "bar", author_id: author.id})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
# just asserting that there is no exception here
|
|
|
|
post
|
|
|
|
|> Ash.Changeset.for_update(:set_title_to_author_profile_description)
|
|
|
|
|> Ash.update!()
|
|
|
|
end
|
|
|
|
|
2024-04-23 10:58:57 +12:00
|
|
|
test "an atomic validation is based on where it appears in the action" do
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{title: "bar"})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
# just asserting that there is no exception here
|
2024-07-09 23:54:50 +12:00
|
|
|
Post
|
|
|
|
|> Ash.Query.filter(id == ^post.id)
|
|
|
|
|> Ash.Query.limit(1)
|
|
|
|
|> Ash.bulk_update!(:change_title_to_foo_unless_its_already_foo, %{})
|
2024-04-23 10:58:57 +12:00
|
|
|
end
|
|
|
|
|
2024-07-13 15:23:42 +12:00
|
|
|
test "an atomic validation can refer to an attribute being cast atomically" do
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{title: "bar"})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
# just asserting that there is no exception here
|
|
|
|
Post
|
|
|
|
|> Ash.Query.filter(id == ^post.id)
|
|
|
|
|> Ash.Query.limit(1)
|
|
|
|
|> Ash.bulk_update!(:update_constrained_int, %{amount: 4})
|
|
|
|
end
|
|
|
|
|
|
|
|
test "an atomic validation can refer to an attribute being cast atomically, and will raise an error" do
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{title: "bar"})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
# just asserting that there is no exception here
|
|
|
|
assert_raise Ash.Error.Invalid, ~r/must be less than or equal to 10/, fn ->
|
|
|
|
Post
|
|
|
|
|> Ash.Query.filter(id == ^post.id)
|
|
|
|
|> Ash.Query.limit(1)
|
|
|
|
|> Ash.bulk_update!(:update_constrained_int, %{amount: 12})
|
|
|
|
end
|
2024-07-15 01:25:49 +12:00
|
|
|
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{title: "bar", constrained_int: 5})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
assert %{constrained_int: 10} = Post.update_constrained_int!(post.id, 5)
|
2024-07-13 15:23:42 +12:00
|
|
|
end
|
|
|
|
|
2023-10-12 08:17:50 +13:00
|
|
|
test "an atomic works with a datetime" do
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
2024-03-28 09:52:28 +13:00
|
|
|
|> Ash.create!()
|
2023-10-12 08:17:50 +13:00
|
|
|
|
|
|
|
now = DateTime.utc_now()
|
|
|
|
|
|
|
|
assert %{created_at: ^now} =
|
|
|
|
post
|
2024-03-28 09:52:28 +13:00
|
|
|
|> Ash.Changeset.new()
|
2023-10-12 08:17:50 +13:00
|
|
|
|> Ash.Changeset.atomic_update(:created_at, expr(^now))
|
2024-03-28 09:52:28 +13:00
|
|
|
|> Ash.Changeset.for_update(:update, %{})
|
|
|
|
|> Ash.update!()
|
2023-10-12 08:17:50 +13:00
|
|
|
end
|
|
|
|
|
2023-08-29 08:18:56 +12:00
|
|
|
test "an atomic that violates a constraint will return the proper error" do
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
2024-03-28 09:52:28 +13:00
|
|
|
|> Ash.create!()
|
2023-08-29 08:18:56 +12:00
|
|
|
|
|
|
|
assert_raise Ash.Error.Invalid, ~r/does not exist/, fn ->
|
|
|
|
post
|
2024-03-28 09:52:28 +13:00
|
|
|
|> Ash.Changeset.new()
|
2023-09-01 03:47:15 +12:00
|
|
|
|> Ash.Changeset.atomic_update(:organization_id, Ash.UUID.generate())
|
2024-03-28 09:52:28 +13:00
|
|
|
|> Ash.Changeset.for_update(:update, %{})
|
|
|
|
|> Ash.update!()
|
2023-08-29 08:18:56 +12:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
test "an atomic can refer to a calculation" do
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
2024-03-28 09:52:28 +13:00
|
|
|
|> Ash.create!()
|
2023-08-29 08:18:56 +12:00
|
|
|
|
|
|
|
post =
|
|
|
|
post
|
|
|
|
|> Ash.Changeset.for_update(:update, %{})
|
2023-09-01 03:47:15 +12:00
|
|
|
|> Ash.Changeset.atomic_update(:score, expr(score_after_winning))
|
2024-03-28 09:52:28 +13:00
|
|
|
|> Ash.update!()
|
2023-08-29 08:18:56 +12:00
|
|
|
|
|
|
|
assert post.score == 1
|
|
|
|
end
|
|
|
|
|
|
|
|
test "an atomic can be attached to an action" do
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
2024-03-28 09:52:28 +13:00
|
|
|
|> Ash.create!()
|
2023-08-29 08:18:56 +12:00
|
|
|
|
|
|
|
assert Post.increment_score!(post, 2).score == 2
|
|
|
|
|
|
|
|
assert Post.increment_score!(post, 2).score == 4
|
|
|
|
end
|
2024-05-16 01:50:36 +12:00
|
|
|
|
2024-05-16 03:15:34 +12:00
|
|
|
test "relationships can be used in atomic update" do
|
2024-05-16 01:50:36 +12:00
|
|
|
author =
|
|
|
|
Author
|
2024-06-18 23:50:54 +12:00
|
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
|
|
first_name: "John",
|
|
|
|
last_name: "Doe"
|
|
|
|
})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
parent_post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{price: 42, author_id: author.id})
|
2024-05-16 01:50:36 +12:00
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
post =
|
|
|
|
Post
|
2024-06-18 23:50:54 +12:00
|
|
|
|> Ash.Changeset.for_create(:create, %{
|
|
|
|
price: 1,
|
|
|
|
author_id: author.id,
|
|
|
|
parent_post_id: parent_post.id
|
|
|
|
})
|
2024-05-16 01:50:36 +12:00
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
post =
|
|
|
|
post
|
|
|
|
|> Ash.Changeset.for_update(:set_title_from_author, %{})
|
|
|
|
|> Ash.update!()
|
|
|
|
|
|
|
|
assert post.title == "John"
|
2024-06-18 22:23:27 +12:00
|
|
|
|
|
|
|
post =
|
|
|
|
post
|
|
|
|
|> Ash.Changeset.for_update(:set_attributes_from_parent, %{})
|
|
|
|
|> Ash.update!()
|
|
|
|
|
|
|
|
assert post.title == "John"
|
2024-05-16 01:50:36 +12:00
|
|
|
end
|
2024-05-16 03:15:34 +12:00
|
|
|
|
|
|
|
test "relationships can be used in atomic update and in an atomic update filter" do
|
|
|
|
author =
|
|
|
|
Author
|
|
|
|
|> Ash.Changeset.for_create(:create, %{first_name: "John", last_name: "Doe"})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{price: 1, author_id: author.id})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Query.filter(author.last_name == "Doe")
|
|
|
|
|> Ash.bulk_update!(:set_title_from_author, %{}, return_records?: true)
|
|
|
|
|> Map.get(:records)
|
|
|
|
|> List.first()
|
|
|
|
|
|
|
|
assert post.title == "John"
|
|
|
|
end
|
|
|
|
|
|
|
|
test "relationships can be used in atomic update and in an atomic update filter when first join is a left join" do
|
|
|
|
author =
|
|
|
|
Author
|
|
|
|
|> Ash.Changeset.for_create(:create, %{first_name: "John", last_name: "Doe"})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{price: 1, author_id: author.id})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
assert [] =
|
|
|
|
Post
|
|
|
|
|> Ash.Query.filter(is_nil(author.last_name))
|
|
|
|
|> Ash.bulk_update!(:set_title_from_author, %{}, return_records?: true)
|
|
|
|
|> Map.get(:records)
|
|
|
|
end
|
2024-07-23 02:29:00 +12:00
|
|
|
|
2024-07-23 22:55:44 +12:00
|
|
|
Enum.each(
|
|
|
|
[
|
|
|
|
:exists,
|
|
|
|
:list,
|
|
|
|
:count,
|
|
|
|
:combined
|
|
|
|
],
|
|
|
|
fn aggregate ->
|
|
|
|
test "can use #{aggregate} in validation" do
|
|
|
|
post =
|
|
|
|
Post
|
|
|
|
|> Ash.Changeset.for_create(:create, %{title: "foo", price: 1})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
Comment
|
|
|
|
|> Ash.Changeset.for_create(:create, %{post_id: post.id, title: "foo"})
|
|
|
|
|> Ash.create!()
|
|
|
|
|
|
|
|
assert_raise Ash.Error.Invalid, ~r/Can only update if Post has no comments/, fn ->
|
|
|
|
post
|
|
|
|
|> Ash.Changeset.new()
|
|
|
|
|> Ash.Changeset.put_context(:aggregate, unquote(aggregate))
|
|
|
|
|> Ash.Changeset.for_update(:update_if_no_comments, %{title: "bar"})
|
|
|
|
|> Ash.update!()
|
|
|
|
end
|
|
|
|
|
|
|
|
assert_raise Ash.Error.Invalid, ~r/Can only update if Post has no comments/, fn ->
|
|
|
|
post
|
|
|
|
|> Ash.Changeset.new()
|
|
|
|
|> Ash.Changeset.put_context(:aggregate, unquote(aggregate))
|
|
|
|
|> Ash.Changeset.for_update(:update_if_no_comments_non_atomic, %{title: "bar"})
|
|
|
|
|> Ash.update!()
|
|
|
|
end
|
|
|
|
|
|
|
|
assert_raise Ash.Error.Invalid, ~r/Can only delete if Post has no comments/, fn ->
|
|
|
|
post
|
|
|
|
|> Ash.Changeset.new()
|
|
|
|
|> Ash.Changeset.put_context(:aggregate, unquote(aggregate))
|
|
|
|
|> Ash.Changeset.for_destroy(:destroy_if_no_comments_non_atomic, %{})
|
|
|
|
|> Ash.destroy!()
|
|
|
|
end
|
|
|
|
|
|
|
|
assert_raise Ash.Error.Invalid, ~r/Can only delete if Post has no comments/, fn ->
|
|
|
|
post
|
|
|
|
|> Ash.Changeset.new()
|
|
|
|
|> Ash.Changeset.put_context(:aggregate, unquote(aggregate))
|
|
|
|
|> Ash.Changeset.for_destroy(:destroy_if_no_comments, %{})
|
|
|
|
|> Ash.destroy!()
|
|
|
|
end
|
|
|
|
end
|
2024-07-23 02:29:00 +12:00
|
|
|
end
|
2024-07-23 22:55:44 +12:00
|
|
|
)
|
2023-08-29 08:18:56 +12:00
|
|
|
end
|