diff --git a/lib/data_layer.ex b/lib/data_layer.ex index 57243028..200ee3af 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -1930,7 +1930,8 @@ defmodule AshPostgres.DataLayer do end) end - defp get_source_for_upsert_field(field, resource) do + @doc false + def get_source_for_upsert_field(field, resource) do case Ash.Resource.Info.attribute(resource, field) do %{source: source} when not is_nil(source) -> source diff --git a/lib/sql_implementation.ex b/lib/sql_implementation.ex index 67819a40..d01c134e 100644 --- a/lib/sql_implementation.ex +++ b/lib/sql_implementation.ex @@ -41,6 +41,33 @@ defmodule AshPostgres.SqlImplementation do {:ok, Ecto.Query.dynamic(fragment("'[]'::jsonb")), acc} end + def expr( + query, + %Ash.Query.UpsertConflict{attribute: attribute}, + _bindings, + _embedded?, + acc, + _type + ) do + query.__ash_bindings__.resource + + {:ok, + Ecto.Query.dynamic( + [], + fragment( + "EXCLUDED.?", + literal( + ^to_string( + AshPostgres.DataLayer.get_source_for_upsert_field( + attribute, + query.__ash_bindings__.resource + ) + ) + ) + ) + ), acc} + end + def expr(query, %AshPostgres.Functions.Binding{}, _bindings, _embedded?, acc, _type) do binding = AshSql.Bindings.get_binding( diff --git a/test/bulk_create_test.exs b/test/bulk_create_test.exs index bdfdd497..d7a2c544 100644 --- a/test/bulk_create_test.exs +++ b/test/bulk_create_test.exs @@ -2,6 +2,8 @@ defmodule AshPostgres.BulkCreateTest do use AshPostgres.RepoCase, async: false alias AshPostgres.Test.{Post, Record} + import Ash.Expr + describe "bulk creates" do test "bulk creates insert each input" do Ash.bulk_create!([%{title: "fred"}, %{title: "george"}], Post, :create) @@ -109,6 +111,51 @@ defmodule AshPostgres.BulkCreateTest do end) end + test "bulk upsert skips with upsert_condition" do + assert [ + {:ok, %{title: "fredfoo", uniq_if_contains_foo: "1foo", price: 10}}, + {:ok, %{title: "georgefoo", uniq_if_contains_foo: "2foo", price: 20}}, + {:ok, %{title: "herbert", uniq_if_contains_foo: "3", price: 30}} + ] = + Ash.bulk_create!( + [ + %{title: "fredfoo", uniq_if_contains_foo: "1foo", price: 10}, + %{title: "georgefoo", uniq_if_contains_foo: "2foo", price: 20}, + %{title: "herbert", uniq_if_contains_foo: "3", price: 30} + ], + Post, + :create, + return_stream?: true, + return_records?: true + ) + |> Enum.sort_by(fn {:ok, result} -> result.title end) + + assert [ + {:ok, %{title: "georgefoo", uniq_if_contains_foo: "2foo", price: 20_000}}, + {:ok, %{title: "herbert", uniq_if_contains_foo: "3", price: 30}} + ] = + Ash.bulk_create!( + [ + %{title: "fredfoo", uniq_if_contains_foo: "1foo", price: 10}, + %{title: "georgefoo", uniq_if_contains_foo: "2foo", price: 20_000}, + %{title: "herbert", uniq_if_contains_foo: "3", price: 30} + ], + Post, + :upsert_with_no_filter, + return_stream?: true, + upsert_condition: expr(price != upsert_conflict(:price)), + return_errors?: true, + return_records?: true + ) + |> Enum.sort_by(fn + {:ok, result} -> + result.title + + _ -> + nil + end) + end + # confirmed that this doesn't work because it can't. An upsert must map to a potentially successful insert. # leaving this test here for posterity # test "bulk creates can upsert with id" do diff --git a/test/support/resources/post.ex b/test/support/resources/post.ex index 8b7fff09..e4f64c23 100644 --- a/test/support/resources/post.ex +++ b/test/support/resources/post.ex @@ -278,6 +278,12 @@ defmodule AshPostgres.Test.Post do end) end + create :upsert_with_no_filter do + upsert?(true) + upsert_identity(:uniq_if_contains_foo) + upsert_fields([:price]) + end + update :set_title_from_author do change(atomic_update(:title, expr(author.first_name))) end