diff --git a/lib/data_layer.ex b/lib/data_layer.ex index fa84faec..e4c36865 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -2375,8 +2375,27 @@ defmodule AshPostgres.DataLayer do fields_to_upsert = case fields_to_upsert do - [] -> keys - fields_to_upsert -> fields_to_upsert + [] -> + keys + + fields_to_upsert -> + # Include fields with update_defaults (e.g. update_timestamp) + # even if they aren't in the changeset attributes or upsert_fields. + # These fields should always be refreshed when an upsert modifies fields. + # Can be disabled via context: %{data_layer: %{touch_update_defaults?: false}} + touch_update_defaults? = + Enum.at(changesets, 0).context[:data_layer][:touch_update_defaults?] != false + + if touch_update_defaults? do + update_default_fields = + update_defaults + |> Keyword.keys() + |> Enum.reject(&(&1 in fields_to_upsert or &1 in keys)) + + fields_to_upsert ++ update_default_fields + else + fields_to_upsert + end end fields_to_upsert diff --git a/test/bulk_create_test.exs b/test/bulk_create_test.exs index 240033a1..1d19a48d 100644 --- a/test/bulk_create_test.exs +++ b/test/bulk_create_test.exs @@ -103,6 +103,137 @@ defmodule AshPostgres.BulkCreateTest do end) end + test "bulk creates with upsert updates update_timestamp" do + past = DateTime.add(DateTime.utc_now(), -60, :second) + + assert [ + {:ok, %{title: "fred", uniq_one: "one", uniq_two: "two"} = initial} + ] = + Ash.bulk_create!( + [ + %{ + title: "fred", + uniq_one: "one", + uniq_two: "two", + price: 10, + updated_at: past + } + ], + Post, + :create, + return_stream?: true, + return_records?: true + ) + |> Enum.to_list() + + assert DateTime.compare(initial.updated_at, past) == :eq + + assert [ + {:ok, %{title: "fred", uniq_one: "one", uniq_two: "two", price: 1000} = upserted} + ] = + Ash.bulk_create!( + [%{title: "something", uniq_one: "one", uniq_two: "two", price: 1000}], + Post, + :create, + upsert?: true, + upsert_identity: :uniq_one_and_two, + upsert_fields: [:price], + return_stream?: true, + return_errors?: true, + return_records?: true + ) + |> Enum.to_list() + + assert DateTime.after?(upserted.updated_at, initial.updated_at) + end + + test "bulk creates with empty upsert does not update update_timestamp" do + past = DateTime.add(DateTime.utc_now(), -60, :second) + + assert [ + {:ok, %{title: "fred", uniq_one: "one", uniq_two: "two"} = initial} + ] = + Ash.bulk_create!( + [ + %{ + title: "fred", + uniq_one: "one", + uniq_two: "two", + price: 10, + updated_at: past + } + ], + Post, + :create, + return_stream?: true, + return_records?: true + ) + |> Enum.to_list() + + assert [ + {:ok, %{title: "fred"} = upserted} + ] = + Ash.bulk_create!( + [%{title: "something", uniq_one: "one", uniq_two: "two", price: 1000}], + Post, + :create, + upsert?: true, + upsert_identity: :uniq_one_and_two, + upsert_fields: [], + return_stream?: true, + return_errors?: true, + return_records?: true + ) + |> Enum.to_list() + + assert DateTime.compare(upserted.updated_at, initial.updated_at) == :eq + end + + test "bulk creates with upsert does not update update_timestamp when touch_update_defaults? is false" do + past = DateTime.add(DateTime.utc_now(), -60, :second) + + assert [ + {:ok, %{title: "fred", uniq_one: "one", uniq_two: "two"} = initial} + ] = + Ash.bulk_create!( + [ + %{ + title: "fred", + uniq_one: "one", + uniq_two: "two", + price: 10, + updated_at: past + } + ], + Post, + :create, + return_stream?: true, + return_records?: true + ) + |> Enum.to_list() + + assert DateTime.compare(initial.updated_at, past) == :eq + + assert [ + {:ok, %{title: "fred", uniq_one: "one", uniq_two: "two", price: 1000} = upserted} + ] = + Ash.bulk_create!( + [%{title: "something", uniq_one: "one", uniq_two: "two", price: 1000}], + Post, + :create, + upsert?: true, + upsert_identity: :uniq_one_and_two, + upsert_fields: [:price], + context: %{data_layer: %{touch_update_defaults?: false}}, + return_stream?: true, + return_errors?: true, + return_records?: true + ) + |> Enum.to_list() + + assert DateTime.compare(upserted.updated_at, initial.updated_at) == :eq + end + test "bulk upsert skips with filter" do assert [ {:ok, %{title: "fredfoo", uniq_if_contains_foo: "1foo", price: 10}},