Skip to content

Commit 3ed809e

Browse files
authored
improvement: read touch_update_defaults? from options instead of changeset context (#701)
fix: bulk_create with upsert now updates update_timestamp fields on conflict update_timestamp attributes (e.g. updated_at) were never included in the ON CONFLICT DO UPDATE SET clause because they have writable?: false and thus never appear in changeset.attributes. This ensures fields with update_defaults are always included when an upsert modifies fields. Closes #696
1 parent f616565 commit 3ed809e

5 files changed

Lines changed: 71 additions & 6 deletions

File tree

lib/data_layer.ex

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2397,9 +2397,12 @@ defmodule AshPostgres.DataLayer do
23972397
# Include fields with update_defaults (e.g. update_timestamp)
23982398
# even if they aren't in the changeset attributes or upsert_fields.
23992399
# These fields should always be refreshed when an upsert modifies fields.
2400-
# Can be disabled via context: %{data_layer: %{touch_update_defaults?: false}}
2400+
# Can be disabled via touch_update_defaults?: false in the changeset
2401+
# context (either in [:private] or [:data_layer]) or via options map
24012402
touch_update_defaults? =
2402-
Enum.at(changesets, 0).context[:data_layer][:touch_update_defaults?] != false
2403+
Map.get(options, :touch_update_defaults?, true) &&
2404+
Enum.at(changesets, 0).context[:private][:touch_update_defaults?] != false &&
2405+
Enum.at(changesets, 0).context[:data_layer][:touch_update_defaults?] != false
24032406

24042407
if touch_update_defaults? do
24052408
update_default_fields =
@@ -3228,12 +3231,21 @@ defmodule AshPostgres.DataLayer do
32283231
else
32293232
keys = keys || Ash.Resource.Info.primary_key(keys)
32303233

3234+
touch_update_defaults? =
3235+
changeset.context[:private][:touch_update_defaults?] != false
3236+
32313237
update_defaults = update_defaults(resource)
32323238

32333239
explicitly_changing_attributes =
32343240
changeset.attributes
32353241
|> Map.keys()
3236-
|> Enum.concat(Keyword.keys(update_defaults))
3242+
|> then(fn attrs ->
3243+
if touch_update_defaults? do
3244+
Enum.concat(attrs, Keyword.keys(update_defaults))
3245+
else
3246+
attrs
3247+
end
3248+
end)
32373249
|> Kernel.--(Map.get(changeset, :defaults, []))
32383250
|> Kernel.--(keys)
32393251

@@ -3248,6 +3260,7 @@ defmodule AshPostgres.DataLayer do
32483260
upsert_keys: keys,
32493261
action_select: changeset.action_select,
32503262
upsert_fields: upsert_fields,
3263+
touch_update_defaults?: touch_update_defaults?,
32513264
return_records?: true
32523265
}) do
32533266
{:ok, []} ->

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ defmodule AshPostgres.MixProject do
185185
# Run "mix help deps" to learn about dependencies.
186186
defp deps do
187187
[
188-
{:ash, ash_version("~> 3.15")},
188+
{:ash, ash_version("~> 3.19")},
189189
{:spark, "~> 2.3 and >= 2.3.4"},
190190
{:ash_sql, ash_sql_version("~> 0.4 and >= 0.4.3")},
191191
{:igniter, "~> 0.6 and >= 0.6.29", optional: true},

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
%{
2-
"ash": {:hex, :ash, "3.18.0", "9548e0db48e3a51132e0ebe8674fec6a7fa2752fc65bae3f546bca4a98d22ec9", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.14 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "22e7cde0856ae4f4024c2d74a5b9d66209fb3959ffe5a018d1c55e86bff90d36"},
2+
"ash": {:hex, :ash, "3.19.1", "b5e933547d948e44d27adaed5737195488292fc2066e7fe60dd3ac83a0c4e54f", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.14 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "697ac3e4fc6080cb03b1e4ee9088cb8a313a5299686ba1aa91efc86ec4028b6e"},
33
"ash_sql": {:hex, :ash_sql, "0.4.5", "30030675ce995570fcedccd3c0671d85beff03cc0c480e7da5002842dccf0277", [:mix], [{:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, ">= 3.13.4 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "131e06e13ebcf06fc8d050267a5b29f6cc8ef6a781712e61a456f17726a64ea5"},
44
"benchee": {:hex, :benchee, "1.5.0", "4d812c31d54b0ec0167e91278e7de3f596324a78a096fd3d0bea68bb0c513b10", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "5b075393aea81b8ae74eadd1c28b1d87e8a63696c649d8293db7c4df3eb67535"},
55
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},

test/bulk_create_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ defmodule AshPostgres.BulkCreateTest do
224224
upsert?: true,
225225
upsert_identity: :uniq_one_and_two,
226226
upsert_fields: [:price],
227-
context: %{data_layer: %{touch_update_defaults?: false}},
227+
touch_update_defaults?: false,
228228
return_stream?: true,
229229
return_errors?: true,
230230
return_records?: true

test/upsert_test.exs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,56 @@ defmodule AshPostgres.Test.UpsertTest do
9393
assert updated_post.id == id
9494
assert Decimal.equal?(updated_post.decimal, Decimal.new(5))
9595
end
96+
97+
test "upsert with touch_update_defaults? false does not update updated_at" do
98+
id = Ash.UUID.generate()
99+
past = DateTime.add(DateTime.utc_now(), -60, :second)
100+
101+
Post
102+
|> Ash.Changeset.for_create(:create, %{
103+
id: id,
104+
title: "title",
105+
updated_at: past
106+
})
107+
|> Ash.create!()
108+
109+
assert [%{updated_at: backdated}] = Ash.read!(Post)
110+
assert DateTime.compare(backdated, past) == :eq
111+
112+
upserted =
113+
Post
114+
|> Ash.Changeset.for_create(:create, %{
115+
id: id,
116+
title: "title2"
117+
})
118+
|> Ash.create!(upsert?: true, touch_update_defaults?: false)
119+
120+
assert DateTime.compare(upserted.updated_at, past) == :eq
121+
end
122+
123+
test "upsert with empty upsert_fields does not update updated_at" do
124+
id = Ash.UUID.generate()
125+
past = DateTime.add(DateTime.utc_now(), -60, :second)
126+
127+
Post
128+
|> Ash.Changeset.for_create(:create, %{
129+
id: id,
130+
title: "title",
131+
updated_at: past
132+
})
133+
|> Ash.create!()
134+
135+
assert [%{updated_at: backdated}] = Ash.read!(Post)
136+
assert DateTime.compare(backdated, past) == :eq
137+
138+
upserted =
139+
Post
140+
|> Ash.Changeset.for_create(:create, %{
141+
id: id,
142+
title: "title2"
143+
})
144+
|> Ash.create!(upsert?: true, upsert_fields: [])
145+
146+
assert DateTime.compare(upserted.updated_at, past) == :eq
147+
end
96148
end

0 commit comments

Comments
 (0)