Skip to content

Commit 84e528c

Browse files
authored
fix: update ash_postgresql to handle the new bulk_create response in Ash v3.5.44 (#632)
1 parent cf0d1df commit 84e528c

File tree

3 files changed

+225
-5
lines changed

3 files changed

+225
-5
lines changed

lib/data_layer.ex

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2047,11 +2047,18 @@ defmodule AshPostgres.DataLayer do
20472047
maybe_create_tenant!(resource, result)
20482048
end
20492049

2050-
Ash.Resource.put_metadata(
2051-
result,
2052-
:bulk_create_index,
2053-
changeset.context.bulk_create.index
2054-
)
2050+
case get_bulk_operation_metadata(changeset, :bulk_create) do
2051+
{index, metadata_key} ->
2052+
Ash.Resource.put_metadata(result, metadata_key, index)
2053+
2054+
nil ->
2055+
# Compatibility fallback
2056+
Ash.Resource.put_metadata(
2057+
result,
2058+
:bulk_create_index,
2059+
changeset.context[:bulk_create][:index]
2060+
)
2061+
end
20552062
end)}
20562063
end
20572064
end
@@ -3638,4 +3645,20 @@ defmodule AshPostgres.DataLayer do
36383645
resource
36393646
end
36403647
end
3648+
3649+
defp get_bulk_operation_metadata(changeset, bulk_action_type) do
3650+
changeset.context
3651+
|> Enum.find_value(fn
3652+
# New format: {{:bulk_create, ref}, value} -> {index, metadata_key}
3653+
{{^bulk_action_type, ref}, value} ->
3654+
{value.index, {:"#{bulk_action_type}_index", ref}}
3655+
3656+
# Fallback for old format: {:bulk_create, value} -> {index, metadata_key}
3657+
{^bulk_action_type, value} when is_map(value) ->
3658+
{value.index, :"#{bulk_action_type}_index"}
3659+
3660+
_ ->
3661+
nil
3662+
end)
3663+
end
36413664
end

test/bulk_create_test.exs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule AshPostgres.BulkCreateTest do
22
use AshPostgres.RepoCase, async: false
33
alias AshPostgres.Test.{Post, Record}
44

5+
require Ash.Query
56
import Ash.Expr
67

78
describe "bulk creates" do
@@ -355,4 +356,150 @@ defmodule AshPostgres.BulkCreateTest do
355356
|> Ash.read!()
356357
end
357358
end
359+
360+
describe "nested bulk operations" do
361+
test "supports bulk_create in after_action callbacks" do
362+
result =
363+
Ash.bulk_create!(
364+
[%{title: "trigger_nested"}],
365+
Post,
366+
:create_with_nested_bulk_create,
367+
return_records?: true,
368+
authorize?: false
369+
)
370+
371+
# Assert the bulk result contains the expected data
372+
assert %Ash.BulkResult{records: [original_post]} = result
373+
assert original_post.title == "trigger_nested"
374+
375+
# Verify all posts that should exist after the nested operation
376+
all_posts =
377+
Post
378+
|> Ash.Query.sort(:title)
379+
|> Ash.read!()
380+
381+
# Should have: 1 original + 2 nested = 3 total posts
382+
assert length(all_posts) == 3
383+
384+
# Verify we have the expected posts with correct titles
385+
post_titles = Enum.map(all_posts, & &1.title) |> Enum.sort()
386+
assert post_titles == ["nested_post_1", "nested_post_2", "trigger_nested"]
387+
388+
# Verify the specific nested posts were created by the after_action callback
389+
nested_posts =
390+
Post
391+
|> Ash.Query.filter(expr(title in ["nested_post_1", "nested_post_2"]))
392+
|> Ash.Query.sort(:title)
393+
|> Ash.read!()
394+
395+
assert length(nested_posts) == 2
396+
assert [%{title: "nested_post_1"}, %{title: "nested_post_2"}] = nested_posts
397+
398+
# Verify that each nested post has proper metadata
399+
Enum.each(nested_posts, fn post ->
400+
assert is_binary(post.id)
401+
assert post.title in ["nested_post_1", "nested_post_2"]
402+
end)
403+
end
404+
405+
test "supports bulk_update in after_action callbacks" do
406+
# Create the original post - the after_action callback will create and update additional posts
407+
result =
408+
Ash.bulk_create!(
409+
[%{title: "trigger_nested_update"}],
410+
Post,
411+
:create_with_nested_bulk_update,
412+
return_records?: true,
413+
authorize?: false
414+
)
415+
416+
# Assert the bulk result contains the expected data
417+
assert %Ash.BulkResult{records: [original_post]} = result
418+
assert original_post.title == "trigger_nested_update"
419+
420+
# Verify all posts that should exist after the nested operations
421+
# The after_action callback should have created 2 posts and updated them
422+
all_posts =
423+
Post
424+
|> Ash.Query.sort(:title)
425+
|> Ash.read!()
426+
427+
# Should have: 1 original + 2 created and updated = 3 total posts
428+
assert length(all_posts) == 3
429+
430+
# Verify the original post still exists
431+
original_posts =
432+
Post
433+
|> Ash.Query.filter(expr(title == "trigger_nested_update"))
434+
|> Ash.read!()
435+
436+
assert length(original_posts) == 1
437+
assert hd(original_posts).title == "trigger_nested_update"
438+
439+
# Verify the nested posts were created and then updated by the after_action callback
440+
updated_posts =
441+
Post
442+
|> Ash.Query.filter(expr(title == "updated_via_nested_bulk"))
443+
|> Ash.read!()
444+
445+
assert length(updated_posts) == 2
446+
447+
# Verify that the updated posts have proper metadata and were actually updated
448+
Enum.each(updated_posts, fn post ->
449+
assert is_binary(post.id)
450+
assert post.title == "updated_via_nested_bulk"
451+
end)
452+
453+
# Verify no posts remain with the intermediate titles (they should have been updated)
454+
intermediate_posts =
455+
Post
456+
|> Ash.Query.filter(expr(title in ["post_to_update_1", "post_to_update_2"]))
457+
|> Ash.read!()
458+
459+
assert intermediate_posts == [],
460+
"Posts should have been updated, not left with intermediate titles"
461+
end
462+
463+
test "nested bulk operations handle metadata indexing correctly" do
464+
# Create multiple posts in the parent bulk operation to test indexing
465+
# Each parent post's after_action callback will create nested posts
466+
result =
467+
Ash.bulk_create!(
468+
[
469+
%{title: "trigger_nested"},
470+
%{title: "trigger_nested_2"}
471+
],
472+
Post,
473+
:create_with_nested_bulk_create,
474+
return_records?: true,
475+
authorize?: false
476+
)
477+
478+
# Assert both parent posts were created
479+
assert %Ash.BulkResult{records: parent_posts} = result
480+
assert length(parent_posts) == 2
481+
482+
parent_titles = Enum.map(parent_posts, & &1.title) |> Enum.sort()
483+
assert parent_titles == ["trigger_nested", "trigger_nested_2"]
484+
485+
# Verify total posts: 2 parent + (2 nested per parent from after_action) = 6 total
486+
all_posts = Post |> Ash.Query.sort(:title) |> Ash.read!()
487+
assert length(all_posts) == 6
488+
489+
# Count posts by type
490+
nested_posts =
491+
Post
492+
|> Ash.Query.filter(expr(title in ["nested_post_1", "nested_post_2"]))
493+
|> Ash.read!()
494+
495+
# Should have 4 nested posts (2 for each parent operation via after_action callbacks)
496+
assert length(nested_posts) == 4
497+
498+
# Verify each nested post has proper structure
499+
Enum.each(nested_posts, fn post ->
500+
assert is_binary(post.id)
501+
assert post.title in ["nested_post_1", "nested_post_2"]
502+
end)
503+
end
504+
end
358505
end

test/support/resources/post.ex

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,56 @@ defmodule AshPostgres.Test.Post do
432432
upsert_fields([:price])
433433
end
434434

435+
create :create_with_nested_bulk_create do
436+
change(
437+
after_action(fn changeset, result, context ->
438+
Ash.bulk_create!(
439+
[%{title: "nested_post_1"}, %{title: "nested_post_2"}],
440+
__MODULE__,
441+
:create,
442+
authorize?: false,
443+
tenant: changeset.tenant,
444+
return_records?: true
445+
)
446+
447+
{:ok, result}
448+
end)
449+
)
450+
end
451+
452+
create :create_with_nested_bulk_update do
453+
change(
454+
after_action(fn changeset, result, context ->
455+
created_posts =
456+
Ash.bulk_create!(
457+
[%{title: "post_to_update_1"}, %{title: "post_to_update_2"}],
458+
__MODULE__,
459+
:create,
460+
authorize?: false,
461+
tenant: changeset.tenant,
462+
return_records?: true
463+
)
464+
465+
post_ids = Enum.map(created_posts.records, & &1.id)
466+
467+
Ash.bulk_update!(
468+
__MODULE__,
469+
:set_title,
470+
%{title: "updated_via_nested_bulk"},
471+
filter: [id: [in: post_ids]],
472+
authorize?: false,
473+
tenant: changeset.tenant
474+
)
475+
476+
{:ok, result}
477+
end)
478+
)
479+
end
480+
481+
update :set_title do
482+
accept([:title])
483+
end
484+
435485
update :set_title_from_author do
436486
change(atomic_update(:title, expr(author.first_name)))
437487
end

0 commit comments

Comments
 (0)