Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
33 changes: 28 additions & 5 deletions lib/data_layer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2047,11 +2047,18 @@ defmodule AshPostgres.DataLayer do
maybe_create_tenant!(resource, result)
end

Ash.Resource.put_metadata(
result,
:bulk_create_index,
changeset.context.bulk_create.index
)
case get_bulk_operation_metadata(changeset, :bulk_create) do
{index, metadata_key} ->
Ash.Resource.put_metadata(result, metadata_key, index)

nil ->
# Compatibility fallback
Ash.Resource.put_metadata(
result,
:bulk_create_index,
changeset.context[:bulk_create][:index]
)
end
end)}
end
end
Expand Down Expand Up @@ -3638,4 +3645,20 @@ defmodule AshPostgres.DataLayer do
resource
end
end

defp get_bulk_operation_metadata(changeset, bulk_action_type) do
changeset.context
|> Enum.find_value(fn
# New format: {{:bulk_create, ref}, value} -> {index, metadata_key}
{{^bulk_action_type, ref}, value} ->
{value.index, {:"#{bulk_action_type}_index", ref}}

# Fallback for old format: {:bulk_create, value} -> {index, metadata_key}
{^bulk_action_type, value} when is_map(value) ->
{value.index, :"#{bulk_action_type}_index"}

_ ->
nil
end)
end
end
147 changes: 147 additions & 0 deletions test/bulk_create_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule AshPostgres.BulkCreateTest do
use AshPostgres.RepoCase, async: false
alias AshPostgres.Test.{Post, Record}

require Ash.Query
import Ash.Expr

describe "bulk creates" do
Expand Down Expand Up @@ -355,4 +356,150 @@ defmodule AshPostgres.BulkCreateTest do
|> Ash.read!()
end
end

describe "nested bulk operations" do
test "supports bulk_create in after_action callbacks" do
result =
Ash.bulk_create!(
[%{title: "trigger_nested"}],
Post,
:create_with_nested_bulk_create,
return_records?: true,
authorize?: false
)

# Assert the bulk result contains the expected data
assert %Ash.BulkResult{records: [original_post]} = result
assert original_post.title == "trigger_nested"

# Verify all posts that should exist after the nested operation
all_posts =
Post
|> Ash.Query.sort(:title)
|> Ash.read!()

# Should have: 1 original + 2 nested = 3 total posts
assert length(all_posts) == 3

# Verify we have the expected posts with correct titles
post_titles = Enum.map(all_posts, & &1.title) |> Enum.sort()
assert post_titles == ["nested_post_1", "nested_post_2", "trigger_nested"]

# Verify the specific nested posts were created by the after_action callback
nested_posts =
Post
|> Ash.Query.filter(expr(title in ["nested_post_1", "nested_post_2"]))
|> Ash.Query.sort(:title)
|> Ash.read!()

assert length(nested_posts) == 2
assert [%{title: "nested_post_1"}, %{title: "nested_post_2"}] = nested_posts

# Verify that each nested post has proper metadata
Enum.each(nested_posts, fn post ->
assert is_binary(post.id)
assert post.title in ["nested_post_1", "nested_post_2"]
end)
end

test "supports bulk_update in after_action callbacks" do
# Create the original post - the after_action callback will create and update additional posts
result =
Ash.bulk_create!(
[%{title: "trigger_nested_update"}],
Post,
:create_with_nested_bulk_update,
return_records?: true,
authorize?: false
)

# Assert the bulk result contains the expected data
assert %Ash.BulkResult{records: [original_post]} = result
assert original_post.title == "trigger_nested_update"

# Verify all posts that should exist after the nested operations
# The after_action callback should have created 2 posts and updated them
all_posts =
Post
|> Ash.Query.sort(:title)
|> Ash.read!()

# Should have: 1 original + 2 created and updated = 3 total posts
assert length(all_posts) == 3

# Verify the original post still exists
original_posts =
Post
|> Ash.Query.filter(expr(title == "trigger_nested_update"))
|> Ash.read!()

assert length(original_posts) == 1
assert hd(original_posts).title == "trigger_nested_update"

# Verify the nested posts were created and then updated by the after_action callback
updated_posts =
Post
|> Ash.Query.filter(expr(title == "updated_via_nested_bulk"))
|> Ash.read!()

assert length(updated_posts) == 2

# Verify that the updated posts have proper metadata and were actually updated
Enum.each(updated_posts, fn post ->
assert is_binary(post.id)
assert post.title == "updated_via_nested_bulk"
end)

# Verify no posts remain with the intermediate titles (they should have been updated)
intermediate_posts =
Post
|> Ash.Query.filter(expr(title in ["post_to_update_1", "post_to_update_2"]))
|> Ash.read!()

assert intermediate_posts == [],
"Posts should have been updated, not left with intermediate titles"
end

test "nested bulk operations handle metadata indexing correctly" do
# Create multiple posts in the parent bulk operation to test indexing
# Each parent post's after_action callback will create nested posts
result =
Ash.bulk_create!(
[
%{title: "trigger_nested"},
%{title: "trigger_nested_2"}
],
Post,
:create_with_nested_bulk_create,
return_records?: true,
authorize?: false
)

# Assert both parent posts were created
assert %Ash.BulkResult{records: parent_posts} = result
assert length(parent_posts) == 2

parent_titles = Enum.map(parent_posts, & &1.title) |> Enum.sort()
assert parent_titles == ["trigger_nested", "trigger_nested_2"]

# Verify total posts: 2 parent + (2 nested per parent from after_action) = 6 total
all_posts = Post |> Ash.Query.sort(:title) |> Ash.read!()
assert length(all_posts) == 6

# Count posts by type
nested_posts =
Post
|> Ash.Query.filter(expr(title in ["nested_post_1", "nested_post_2"]))
|> Ash.read!()

# Should have 4 nested posts (2 for each parent operation via after_action callbacks)
assert length(nested_posts) == 4

# Verify each nested post has proper structure
Enum.each(nested_posts, fn post ->
assert is_binary(post.id)
assert post.title in ["nested_post_1", "nested_post_2"]
end)
end
end
end
50 changes: 50 additions & 0 deletions test/support/resources/post.ex
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,56 @@ defmodule AshPostgres.Test.Post do
upsert_fields([:price])
end

create :create_with_nested_bulk_create do
change(
after_action(fn changeset, result, context ->
Ash.bulk_create!(
[%{title: "nested_post_1"}, %{title: "nested_post_2"}],
__MODULE__,
:create,
authorize?: false,
tenant: changeset.tenant,
return_records?: true
)

{:ok, result}
end)
)
end

create :create_with_nested_bulk_update do
change(
after_action(fn changeset, result, context ->
created_posts =
Ash.bulk_create!(
[%{title: "post_to_update_1"}, %{title: "post_to_update_2"}],
__MODULE__,
:create,
authorize?: false,
tenant: changeset.tenant,
return_records?: true
)

post_ids = Enum.map(created_posts.records, & &1.id)

Ash.bulk_update!(
__MODULE__,
:set_title,
%{title: "updated_via_nested_bulk"},
filter: [id: [in: post_ids]],
authorize?: false,
tenant: changeset.tenant
)

{:ok, result}
end)
)
end

update :set_title do
accept([:title])
end

update :set_title_from_author do
change(atomic_update(:title, expr(author.first_name)))
end
Expand Down
Loading