Skip to content

Commit

Permalink
fix: load relationships on bulk operations (#1234)
Browse files Browse the repository at this point in the history
This change validates that the `load` statement of bulk operations is
respected when specified, and correctly loads relationships.

Loading relationships with pagination on results for bulk destroys is
still not supported. Indeed, relationships are currently queried using a
lateral join but after the resource deletion has happened, so it looks
like nothing is related.

Signed-off-by: Davide Briani <[email protected]>
  • Loading branch information
davidebriani authored Jun 11, 2024
1 parent 55457d4 commit cd06f91
Show file tree
Hide file tree
Showing 4 changed files with 692 additions and 10 deletions.
62 changes: 52 additions & 10 deletions lib/ash/actions/destroy/bulk.ex
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ defmodule Ash.Actions.Destroy.Bulk do
{atomic_changeset, opts} =
Ash.Actions.Helpers.set_context_and_get_opts(domain, atomic_changeset, opts)

atomic_changeset = Ash.Actions.Helpers.apply_opts_load(atomic_changeset, opts)

atomic_changeset = %{atomic_changeset | domain: domain}

atomic_changeset =
Expand Down Expand Up @@ -503,6 +505,21 @@ defmodule Ash.Actions.Destroy.Bulk do
{results, []}
end

{results, errors, error_count} =
case load_data(
results,
atomic_changeset.domain,
atomic_changeset.resource,
atomic_changeset,
opts
) do
{:ok, results} ->
{results, [], 0}

{:error, error} ->
{[], List.wrap(error), Enum.count(List.wrap(error))}
end

notifications =
if opts[:notify?] do
notifications ++
Expand All @@ -513,12 +530,29 @@ defmodule Ash.Actions.Destroy.Bulk do
notifications
end

status =
case {error_count, results} do
{0, []} ->
:success

{0, _results} ->
:success

{_error_count, []} ->
:error
end

%Ash.BulkResult{
status: :success,
error_count: 0,
status: status,
error_count: error_count,
notifications: notifications,
errors: [],
records: results
errors: errors,
records:
if opts[:return_records?] do
results
else
[]
end
}

{:error, :no_rollback,
Expand Down Expand Up @@ -1913,19 +1947,27 @@ defmodule Ash.Actions.Destroy.Bulk do
resource |> Ash.Resource.Info.public_attributes() |> Enum.map(& &1.name)
end

case List.wrap(changeset.load) ++ select do
[] ->
{:ok, records}

load ->
case Ash.load(records, select,
reuse_values?: true,
domain: domain,
actor: opts[:actor],
authorize?: opts[:authorize?],
tracer: opts[:tracer]
) do
{:ok, records} ->
Ash.load(
records,
load,
List.wrap(changeset.load),
reuse_values?: true,
domain: domain,
actor: opts[:actor],
authorize?: opts[:authorize?],
tracer: opts[:tracer]
)
|> Ash.Actions.Helpers.select(changeset)

other ->
other
end
end

Expand Down
256 changes: 256 additions & 0 deletions test/actions/bulk/bulk_create_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,75 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end
end

defmodule Author do
@moduledoc false
use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets

ets do
private?(true)
end

actions do
default_accept :*
defaults [:read, :create, :update, :destroy]

create :create_with_posts do
argument :post_ids, {:array, :uuid} do
allow_nil? false
constraints min_length: 1
end

change manage_relationship(:post_ids, :posts, type: :append)
end
end

attributes do
uuid_primary_key :id

attribute :name, :string do
public?(true)
end
end

relationships do
has_many :posts, Ash.Test.Actions.BulkCreateTest.Post,
destination_attribute: :author_id,
public?: true
end
end

defmodule PostLink do
@moduledoc false
use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets

ets do
private?(true)
end

attributes do
attribute :type, :string do
public?(true)
end
end

actions do
default_accept :*
defaults [:read, :destroy, create: :*, update: :*]
end

relationships do
belongs_to :source_post, Ash.Test.Actions.BulkCreateTest.Post,
primary_key?: true,
allow_nil?: false,
public?: true

belongs_to :destination_post, Ash.Test.Actions.BulkCreateTest.Post,
primary_key?: true,
allow_nil?: false,
public?: true
end
end

defmodule Post do
@moduledoc false
use Ash.Resource,
Expand Down Expand Up @@ -96,6 +165,15 @@ defmodule Ash.Test.Actions.BulkCreateTest do
default_accept :*
defaults [:read, :destroy, create: :*, update: :*]

create :create_with_related_posts do
argument :related_post_ids, {:array, :uuid} do
allow_nil? false
constraints min_length: 1
end

change manage_relationship(:related_post_ids, :related_posts, type: :append)
end

create :create_with_change do
change fn changeset, _ ->
title = Ash.Changeset.get_attribute(changeset, :title)
Expand Down Expand Up @@ -191,6 +269,14 @@ defmodule Ash.Test.Actions.BulkCreateTest do
allow_nil? false
attribute_public? false
end

belongs_to :author, Author, public?: true

many_to_many :related_posts, __MODULE__,
through: PostLink,
source_attribute_on_join_resource: :source_post_id,
destination_attribute_on_join_resource: :destination_post_id,
public?: true
end
end

Expand Down Expand Up @@ -844,4 +930,174 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end)
end
end

describe "load" do
test "allows loading has_many relationship" do
org = Ash.create!(Org, %{})
post1 = Ash.create!(Post, %{title: "Post 1"}, tenant: org.id, authorize?: false)
post2 = Ash.create!(Post, %{title: "Post 2"}, tenant: org.id, authorize?: false)

load_query =
Post
|> Ash.Query.sort(title: :asc)
|> Ash.Query.select([:title])

assert %Ash.BulkResult{records: [author]} =
Ash.bulk_create!(
[%{name: "Author", post_ids: [post2.id, post1.id]}],
Author,
:create_with_posts,
return_records?: true,
return_errors?: true,
authorize?: false,
tenant: org.id,
load: [posts: load_query]
)

assert [%Post{title: "Post 1"}, %Post{title: "Post 2"}] = author.posts
end

test "allows loading paginated has_many relationship" do
org = Ash.create!(Org, %{})
post1 = Ash.create!(Post, %{title: "Post 1"}, tenant: org.id, authorize?: false)
post2 = Ash.create!(Post, %{title: "Post 2"}, tenant: org.id, authorize?: false)

offset_pagination_query =
Post
|> Ash.Query.sort(title: :asc)
|> Ash.Query.select([:title])
|> Ash.Query.page(count: true, limit: 1)

assert %Ash.BulkResult{records: [author]} =
Ash.bulk_create!(
[%{name: "Author 1", post_ids: [post2.id, post1.id]}],
Author,
:create_with_posts,
return_records?: true,
return_errors?: true,
authorize?: false,
tenant: org.id,
load: [posts: offset_pagination_query]
)

assert %Ash.Page.Offset{
results: [%Post{title: "Post 1", __metadata__: %{keyset: keyset}}],
limit: 1,
offset: 0,
count: 2,
more?: true
} = author.posts

keyset_pagination_query =
Post
|> Ash.Query.sort(title: :asc)
|> Ash.Query.select([:title])
|> Ash.Query.page(count: true, limit: 1, after: keyset)

assert %Ash.BulkResult{records: [author]} =
Ash.bulk_create!(
[%{name: "Author 2", post_ids: [post2.id, post1.id]}],
Author,
:create_with_posts,
return_records?: true,
return_errors?: true,
authorize?: false,
tenant: org.id,
load: [posts: keyset_pagination_query]
)

assert %Ash.Page.Keyset{
results: [%Post{title: "Post 2"}],
limit: 1,
count: 2,
more?: false,
before: nil,
after: ^keyset
} = author.posts
end

test "allows loading many_to_many relationship" do
org = Ash.create!(Org, %{})
related_post1 = Ash.create!(Post, %{title: "Related 1"}, tenant: org.id, authorize?: false)
related_post2 = Ash.create!(Post, %{title: "Related 2"}, tenant: org.id, authorize?: false)

load_query =
Post
|> Ash.Query.sort(title: :asc)
|> Ash.Query.select([:title])

assert %Ash.BulkResult{records: [post]} =
Ash.bulk_create!(
[%{title: "Title", related_post_ids: [related_post2.id, related_post1.id]}],
Post,
:create_with_related_posts,
return_records?: true,
return_errors?: true,
authorize?: false,
tenant: org.id,
load: [related_posts: load_query]
)

assert [%Post{title: "Related 1"}, %Post{title: "Related 2"}] = post.related_posts
end

test "allows loading paginated many_to_many relationship" do
org = Ash.create!(Org, %{})
related_post1 = Ash.create!(Post, %{title: "Related 1"}, tenant: org.id, authorize?: false)
related_post2 = Ash.create!(Post, %{title: "Related 2"}, tenant: org.id, authorize?: false)

offset_pagination_query =
Post
|> Ash.Query.sort(title: :asc)
|> Ash.Query.select([:title])
|> Ash.Query.page(count: true, limit: 1)

assert %Ash.BulkResult{records: [post]} =
Ash.bulk_create!(
[%{title: "Post 1", related_post_ids: [related_post2.id, related_post1.id]}],
Post,
:create_with_related_posts,
return_records?: true,
return_errors?: true,
authorize?: false,
tenant: org.id,
load: [related_posts: offset_pagination_query]
)

assert %Ash.Page.Offset{
results: [%Post{title: "Related 1", __metadata__: %{keyset: keyset}}],
limit: 1,
offset: 0,
count: 2,
more?: true
} = post.related_posts

keyset_pagination_query =
Post
|> Ash.Query.sort(title: :asc)
|> Ash.Query.select([:title])
|> Ash.Query.page(count: true, limit: 1, after: keyset)

assert %Ash.BulkResult{records: [post]} =
Ash.bulk_create!(
[%{title: "Post 2", related_post_ids: [related_post2.id, related_post1.id]}],
Post,
:create_with_related_posts,
return_records?: true,
return_errors?: true,
authorize?: false,
tenant: org.id,
load: [related_posts: keyset_pagination_query]
)

assert %Ash.Page.Keyset{
results: [%Post{title: "Related 2"}],
limit: 1,
count: 2,
more?: false,
before: nil,
after: ^keyset
} = post.related_posts
end
end
end
Loading

0 comments on commit cd06f91

Please sign in to comment.