From d2a5d68bac3f05b9168b07ae65f151cd02c2c88c Mon Sep 17 00:00:00 2001 From: Barnabas Jovanovics Date: Wed, 15 Oct 2025 11:13:54 +0200 Subject: [PATCH 1/2] fix: handle results that can't be mapped to the changeset in bulk_create If the identity used has attibutes that can be generated by the datalayer, we can't map the result back to the changeset and we need to just zip the results with the changesets and return them that way. --- lib/data_layer.ex | 68 ++++++++++++++++++++++++++++++--------- test/bulk_create_test.exs | 14 +++++++- 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/lib/data_layer.ex b/lib/data_layer.ex index 3bad7f45..2c428990 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -2123,35 +2123,63 @@ defmodule AshPostgres.DataLayer do {Map.take(r, keys), r} end) - results = - changesets - |> Enum.map(fn changeset -> - identity = - changeset.attributes - |> Map.take(keys) + cant_map_results_to_changesets = + any_generated_keys_missing?(keys, resource, changesets) - result_for_changeset = Map.get(results_by_identity, identity) - - if result_for_changeset do + results = + if cant_map_results_to_changesets do + results + |> Enum.zip(changesets) + |> Enum.map(fn {result, changeset} -> if !opts[:upsert?] do - maybe_create_tenant!(resource, result_for_changeset) + maybe_create_tenant!(resource, result) end case get_bulk_operation_metadata(changeset) do {index, metadata_key} -> - Ash.Resource.put_metadata(result_for_changeset, metadata_key, index) + Ash.Resource.put_metadata(result, metadata_key, index) nil -> # Compatibility fallback Ash.Resource.put_metadata( - result_for_changeset, + result, :bulk_create_index, changeset.context[:bulk_create][:index] ) end - end - end) - |> Enum.filter(& &1) + end) + else + changesets + |> Enum.map(fn changeset -> + identity = + changeset.attributes + |> Map.take(keys) + + result_for_changeset = Map.get(results_by_identity, identity) + + if result_for_changeset do + if !opts[:upsert?] do + maybe_create_tenant!(resource, result_for_changeset) + end + + case get_bulk_operation_metadata(changeset) do + {index, metadata_key} -> + Ash.Resource.put_metadata(result_for_changeset, metadata_key, index) + + nil -> + # Compatibility fallback + Ash.Resource.put_metadata( + result_for_changeset, + :bulk_create_index, + changeset.context[:bulk_create][:index] + ) + end + end + end) + |> Enum.concat(results) + |> Enum.filter(& &1) + |> Enum.uniq_by(&Map.take(&1, keys)) + end {:ok, results} end @@ -3737,6 +3765,16 @@ defmodule AshPostgres.DataLayer do end end + # checks if any of the attributes in the list of keys are generated and missing from any of the changesets + # if so, we can't match the created record to the changeset by the identity and just need to zip the return + # values with the changesets + defp any_generated_keys_missing?(keys, resource, changesets) do + Enum.any?(keys, fn key -> + Ash.Resource.Info.attribute(resource, key).generated? && + Enum.any?(changesets, fn changeset -> is_nil(changeset.attributes[key]) end) + end) + end + defp get_bulk_operation_metadata(changeset) do changeset.context |> Enum.find_value(fn diff --git a/test/bulk_create_test.exs b/test/bulk_create_test.exs index 296742fd..c18d8d1a 100644 --- a/test/bulk_create_test.exs +++ b/test/bulk_create_test.exs @@ -4,7 +4,7 @@ defmodule AshPostgres.BulkCreateTest do use AshPostgres.RepoCase, async: false - alias AshPostgres.Test.{Post, Record} + alias AshPostgres.Test.{IntegerPost, Post, Record} require Ash.Query import Ash.Expr @@ -356,6 +356,18 @@ defmodule AshPostgres.BulkCreateTest do |> Ash.Query.load(:ratings) |> Ash.read!() end + + test "bulk creates with integer primary key return records" do + %Ash.BulkResult{records: records} = + Ash.bulk_create!( + [%{title: "first"}, %{title: "second"}, %{title: "third"}], + IntegerPost, + :create, + return_records?: true + ) + + assert length(records) == 3 + end end describe "validation errors" do From 522191c7ead7b859d89ba754040511a2bd479f69 Mon Sep 17 00:00:00 2001 From: Barnabas Jovanovics Date: Thu, 16 Oct 2025 10:55:22 +0200 Subject: [PATCH 2/2] refactor: do a simple check for `upsert?` instead --- lib/data_layer.ex | 59 ++++++++++++++++++----------------------------- 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/lib/data_layer.ex b/lib/data_layer.ex index 2c428990..b984c4db 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -2123,32 +2123,8 @@ defmodule AshPostgres.DataLayer do {Map.take(r, keys), r} end) - cant_map_results_to_changesets = - any_generated_keys_missing?(keys, resource, changesets) - results = - if cant_map_results_to_changesets do - results - |> Enum.zip(changesets) - |> Enum.map(fn {result, changeset} -> - if !opts[:upsert?] do - maybe_create_tenant!(resource, result) - end - - case get_bulk_operation_metadata(changeset) 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) - else + if opts[:upsert?] do changesets |> Enum.map(fn changeset -> identity = @@ -2176,9 +2152,28 @@ defmodule AshPostgres.DataLayer do end end end) - |> Enum.concat(results) |> Enum.filter(& &1) - |> Enum.uniq_by(&Map.take(&1, keys)) + else + results + |> Enum.zip(changesets) + |> Enum.map(fn {result, changeset} -> + if !opts[:upsert?] do + maybe_create_tenant!(resource, result) + end + + case get_bulk_operation_metadata(changeset) 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 {:ok, results} @@ -3765,16 +3760,6 @@ defmodule AshPostgres.DataLayer do end end - # checks if any of the attributes in the list of keys are generated and missing from any of the changesets - # if so, we can't match the created record to the changeset by the identity and just need to zip the return - # values with the changesets - defp any_generated_keys_missing?(keys, resource, changesets) do - Enum.any?(keys, fn key -> - Ash.Resource.Info.attribute(resource, key).generated? && - Enum.any?(changesets, fn changeset -> is_nil(changeset.attributes[key]) end) - end) - end - defp get_bulk_operation_metadata(changeset) do changeset.context |> Enum.find_value(fn