diff --git a/lib/ash/actions/destroy/bulk.ex b/lib/ash/actions/destroy/bulk.ex index 4f282d91..522d918d 100644 --- a/lib/ash/actions/destroy/bulk.ex +++ b/lib/ash/actions/destroy/bulk.ex @@ -98,6 +98,9 @@ defmodule Ash.Actions.Destroy.Bulk do changeset = opts[:atomic_changeset] -> changeset + query.action.manual -> + {:not_atomic, "Manual read actions cannot be updated atomically"} + Ash.DataLayer.data_layer_can?(query.resource, :destroy_query) -> private_context = Map.new(Keyword.take(opts, [:actor, :tenant, :authorize])) @@ -529,7 +532,7 @@ defmodule Ash.Actions.Destroy.Bulk do {results, []} end - {results, errors, error_count} = + {results, errors, error_count, notifications} = case load_data( results, atomic_changeset.domain, @@ -538,7 +541,36 @@ defmodule Ash.Actions.Destroy.Bulk do opts ) do {:ok, results} -> - {results, [], 0} + if Enum.empty?(atomic_changeset.after_action) do + {results, [], 0, notifications} + else + Enum.reduce(results, {[], [], 0, notifications}, fn result, + {results, errors, + error_count, + notifications} -> + case Ash.Changeset.run_after_actions(result, atomic_changeset, []) do + {:error, error} -> + if opts[:transaction] && opts[:rollback_on_error?] do + if Ash.DataLayer.in_transaction?(atomic_changeset.resource) do + Ash.DataLayer.rollback( + atomic_changeset.resource, + error + ) + end + end + + {results, errors ++ List.wrap(error), + error_count + Enum.count(List.wrap(error)), notifications} + + {:ok, result, _changeset, %{notifications: more_new_notifications}} -> + {[result | results], errors, error_count, + notifications ++ more_new_notifications} + end + end) + |> then(fn {results, errors, error_count, notifications} -> + {Enum.reverse(results), errors, error_count, notifications} + end) + end {:error, error} -> {[], List.wrap(error), Enum.count(List.wrap(error))} @@ -708,6 +740,8 @@ defmodule Ash.Actions.Destroy.Bulk do resource = opts[:resource] opts = Ash.Actions.Helpers.set_opts(opts, domain) + read_action = get_read_action(resource, opts) + {context_cs, opts} = Ash.Actions.Helpers.set_context_and_get_opts(domain, Ash.Changeset.new(resource), opts) @@ -722,6 +756,9 @@ defmodule Ash.Actions.Destroy.Bulk do !Ash.Resource.Info.primary_action(resource, :read) -> {:not_atomic, "cannot atomically destroy a stream without a primary read action"} + read_action.manual -> + {:not_atomic, "Manual read actions cannot be updated atomically"} + Ash.DataLayer.data_layer_can?(resource, :destroy_query) -> opts = Keyword.update( @@ -822,8 +859,10 @@ defmodule Ash.Actions.Destroy.Bulk do fn batch -> pkeys = [or: Enum.map(batch, &Map.take(&1, pkey))] + read_action = get_read_action(resource, opts).name + resource - |> Ash.Query.for_read(Ash.Resource.Info.primary_action!(resource, :read).name, %{}, + |> Ash.Query.for_read(read_action, %{}, actor: opts[:actor], authorize?: false, context: atomic_changeset.context, @@ -852,7 +891,9 @@ defmodule Ash.Actions.Destroy.Bulk do resource: opts[:resource], return_notifications?: opts[:return_notifications?], notify?: opts[:notify?], + read_action: read_action, return_records?: opts[:return_records?], + allow_stream_with: opts[:allow_stream_with], strategy: [:atomic] ], not_atomic_reason @@ -2319,4 +2360,14 @@ defmodule Ash.Actions.Destroy.Bulk do context ) end + + defp get_read_action(resource, opts) do + case opts[:read_action] do + nil -> + Ash.Resource.Info.primary_action!(resource, :read) + + action -> + Ash.Resource.Info.action(resource, action) + end + end end diff --git a/lib/ash/actions/update/bulk.ex b/lib/ash/actions/update/bulk.ex index 2ef982e9..7f29f3c0 100644 --- a/lib/ash/actions/update/bulk.ex +++ b/lib/ash/actions/update/bulk.ex @@ -53,6 +53,9 @@ defmodule Ash.Actions.Update.Bulk do :atomic not in opts[:strategy] -> {:not_atomic, "Not in requested strategies"} + query.action.manual -> + {:not_atomic, "Manual read actions cannot be updated atomically"} + changeset = opts[:atomic_changeset] -> changeset @@ -901,6 +904,9 @@ defmodule Ash.Actions.Update.Bulk do !read_action -> {:not_atomic, "cannot atomically update a stream without a primary read action"} + read_action.manual -> + {:not_atomic, "Manual read actions cannot be updated atomically"} + Ash.DataLayer.data_layer_can?(resource, :update_query) -> opts = Keyword.update( @@ -1007,8 +1013,10 @@ defmodule Ash.Actions.Update.Bulk do fn batch -> pkeys = [or: Enum.map(batch, &Map.take(&1, pkey))] + read_action = get_read_action(resource, opts).name + resource - |> Ash.Query.for_read(get_read_action(resource, opts).name, %{}, + |> Ash.Query.for_read(read_action, %{}, actor: opts[:actor], authorize?: false, context: atomic_changeset.context, @@ -1036,6 +1044,8 @@ defmodule Ash.Actions.Update.Bulk do return_notifications?: opts[:return_notifications?], notify?: opts[:notify?], return_records?: opts[:return_records?], + allow_stream_with: opts[:allow_stream_with], + read_action: read_action, strategy: [:atomic] ) |> case do diff --git a/mix.exs b/mix.exs index f588f047..a856bb20 100644 --- a/mix.exs +++ b/mix.exs @@ -361,7 +361,7 @@ defmodule Ash.MixProject do {:simple_sat, "~> 0.1 and >= 0.1.1", optional: true}, # Code Generators - {:igniter, "~> 0.2.7"}, + {:igniter, "~> 0.2.12"}, # Dev/Test dependencies {:eflame, "~> 1.0", only: [:dev, :test]}, diff --git a/mix.lock b/mix.lock index 60176bed..7a9ae6a5 100644 --- a/mix.lock +++ b/mix.lock @@ -22,7 +22,7 @@ "git_ops": {:hex, :git_ops, "2.6.1", "cc7799a68c26cf814d6d1a5121415b4f5bf813de200908f930b27a2f1fe9dad5", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ce62d07e41fe993ec22c35d5edb11cf333a21ddaead6f5d9868fcb607d42039e"}, "glob_ex": {:hex, :glob_ex, "0.1.7", "eae6b6377147fb712ac45b360e6dbba00346689a87f996672fe07e97d70597b1", [:mix], [], "hexpm", "decc1c21c0c73df3c9c994412716345c1692477b9470e337f628a7e08da0da6a"}, "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, - "igniter": {:hex, :igniter, "0.2.7", "75524d8bbdb67eb7ccf39e583e5781f7d46f48f06b3450811602854da078e978", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "2962a8ca961c96ef1e2a13c9bb42d44b6f74dd160d3bd4f0f23032ea8b199c43"}, + "igniter": {:hex, :igniter, "0.2.12", "e2e8fbb15effecb433f4096edbb0754282553544c75c3130d06ca09bdaa1fb13", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "51f3487a13441cd3e6e0d559689f8b0ba2c716834f86802e8a6760fdd1a2e579"}, "jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, diff --git a/test/uuid_v7_test.exs b/test/uuid_v7_test.exs index 26413b90..a459bad8 100644 --- a/test/uuid_v7_test.exs +++ b/test/uuid_v7_test.exs @@ -11,7 +11,11 @@ defmodule Ash.Test.UUIDv7Test do test "generate/1 is ordered" do uuids = for _ <- 1..10_000 do - Ash.UUIDv7.generate() + uuid = Ash.UUIDv7.generate() + # only guaranteed sorted if >= 1 nanosecond apart + # can't sleep for one nanoseond AFAIK, so sleep for 1 ms + :timer.sleep(1) + uuid end assert uuids == Enum.sort(uuids)