improvement: bulk destroy for handling array changes in embeds

improvement: set `__union_tag__` constraint in array handlers for unions
fix: sleep to avoid uuidv7 specifity flaky test
test: remove unused variable in tests
This commit is contained in:
Zach Daniel 2024-06-24 09:48:08 -04:00
parent ec4ff55529
commit 83434b0d1a
4 changed files with 179 additions and 175 deletions

View file

@ -731,19 +731,14 @@ defmodule Ash.EmbeddableType do
def handle_change_array(old_values, new_values, constraints) do def handle_change_array(old_values, new_values, constraints) do
pkey_fields = Ash.Resource.Info.primary_key(__MODULE__) pkey_fields = Ash.Resource.Info.primary_key(__MODULE__)
destroy_action =
constraints[:destroy_action] ||
Ash.Resource.Info.primary_action!(__MODULE__, :destroy).name
old_values old_values
|> List.wrap() |> List.wrap()
|> Enum.with_index()
|> then(fn list -> |> then(fn list ->
if Enum.empty?(pkey_fields) do if Enum.empty?(pkey_fields) do
list list
else else
list list
|> Enum.reject(fn {old_value, _} -> |> Enum.reject(fn old_value ->
pkey = Map.take(old_value, pkey_fields) pkey = Map.take(old_value, pkey_fields)
Enum.any?(new_values, fn new_value -> Enum.any?(new_values, fn new_value ->
@ -752,33 +747,45 @@ defmodule Ash.EmbeddableType do
end) end)
end end
end) end)
|> Enum.reduce_while(:ok, fn {record, index}, :ok ->
record
|> Ash.Changeset.new()
|> Ash.EmbeddableType.copy_source(constraints)
|> Ash.Changeset.for_destroy(destroy_action, %{}, domain: ShadowDomain)
|> Ash.destroy()
|> case do |> case do
:ok -> [] ->
{:cont, :ok}
{:error, error} ->
errors =
error
|> Ash.EmbeddableType.handle_errors()
|> Enum.map(fn keyword ->
Keyword.put(keyword, :index, index)
end)
{:halt, {:error, errors}}
end
end)
|> case do
:ok ->
{:ok, new_values} {:ok, new_values}
{:error, error} -> to_destroy ->
{:error, error} destroy_action =
constraints[:destroy_action] ||
Ash.Resource.Info.primary_action!(__MODULE__, :destroy).name
{context, opts} =
case constraints[:__source__] do
%Ash.Changeset{context: context} = source ->
{Map.put(context, :__source__, source),
Ash.Context.to_opts(context[:private] || %{})}
_ ->
{%{}, []}
end
case Ash.bulk_destroy(
to_destroy,
destroy_action,
%{},
Keyword.merge(opts,
domain: ShadowDomain,
context: context,
sorted?: true,
skip_unknown_inputs: skip_unknown_inputs(constraints),
return_records?: true,
return_errors?: true,
batch_size: 1_000_000_000
)
) do
%Ash.BulkResult{status: :success} ->
{:ok, new_values}
%Ash.BulkResult{errors: errors} ->
{:error, Ash.EmbeddableType.handle_errors(errors)}
end
end end
end end

View file

@ -390,39 +390,15 @@ defmodule Ash.Type.Union do
def cast_input(nil, _), do: {:ok, nil} def cast_input(nil, _), do: {:ok, nil}
def cast_input(%Ash.Union{value: value, type: type_name}, constraints) do def cast_input(%Ash.Union{value: value, type: type_name}, constraints) do
type = constraints[:types][type_name][:type] case try_cast_type(value, constraints, type_name, constraints[:types][type_name], [], false) do
inner_constraints = constraints[:types][type_name][:constraints] || [] {_, {:ok, value}} ->
{:ok, value}
inner_constraints = {_, {:expose_error, errors}} ->
if Ash.Type.embedded_type?(type) do {:error, errors}
case constraints[:__source__] do
%Ash.Changeset{} = source ->
Keyword.put(inner_constraints, :__source__, source)
_ -> {_, {:error, error}} ->
inner_constraints {:error, error_message(error)}
end
|> Keyword.put(:__union_tag__, constraints[:types][type_name][:tag])
else
inner_constraints
end
case Ash.Type.cast_input(
type,
value,
inner_constraints
) do
{:ok, value} ->
case Ash.Type.apply_constraints(type, value, inner_constraints) do
{:ok, value} ->
{:ok, %Ash.Union{value: value, type: type_name}}
{:error, other} ->
{:error, other}
end
error ->
error
end end
end end
@ -431,6 +407,27 @@ defmodule Ash.Type.Union do
types types
|> Enum.reduce_while({:error, []}, fn {type_name, config}, {:error, errors} -> |> Enum.reduce_while({:error, []}, fn {type_name, config}, {:error, errors} ->
try_cast_type(value, constraints, type_name, config, errors)
end)
|> case do
{:error, errors} when is_binary(errors) ->
{:error, errors}
{:error, errors} ->
{:error, error_message(errors)}
{:expose_error, errors} ->
{:error, errors}
{:ok, value} ->
{:ok, value}
value ->
value
end
end
defp try_cast_type(value, constraints, type_name, config, errors, tags_must_match? \\ true) do
type = config[:type] type = config[:type]
if is_map(value) && config[:tag] do if is_map(value) && config[:tag] do
@ -457,11 +454,7 @@ defmodule Ash.Type.Union do
value value
end end
their_tag_value = get_tag(value, config[:tag]) if !tags_must_match? || tags_equal?(tag_value, get_tag(value, config[:tag])) do
tags_equal? = tags_equal?(tag_value, their_tag_value)
if tags_equal? do
value = value =
if Keyword.get(config, :cast_tag?, true) do if Keyword.get(config, :cast_tag?, true) do
value value
@ -540,23 +533,6 @@ defmodule Ash.Type.Union do
end end
end end
end end
end)
|> case do
{:error, errors} when is_binary(errors) ->
{:error, errors}
{:error, errors} ->
{:error, error_message(errors)}
{:expose_error, errors} ->
{:error, errors}
{:ok, value} ->
{:ok, value}
value ->
value
end
end end
defp get_tag(map, tag) do defp get_tag(map, tag) do
@ -830,10 +806,18 @@ defmodule Ash.Type.Union do
end) end)
type = constraints[:types][name][:type] type = constraints[:types][name][:type]
type_constraints = constraints[:types][name][:constraints] || []
type_constraints =
if union_tag = constraints[:types][name][:tag] do
Keyword.put(type_constraints, :__union_tag__, union_tag)
else
type_constraints
end
item_constraints = item_constraints =
Ash.Type.include_source({:array, type}, constraints[:__source__], Ash.Type.include_source({:array, type}, constraints[:__source__],
items: constraints[:types][name][:constraints] || [] items: type_constraints
) )
result = result =
@ -931,13 +915,21 @@ defmodule Ash.Type.Union do
new_values = Enum.map(new_values, & &1.value) new_values = Enum.map(new_values, & &1.value)
type = constraints[:types][name][:type] type = constraints[:types][name][:type]
type_constraints = constraints[:types][name][:constraints] || []
type_constraints =
if union_tag = constraints[:types][name][:tag] do
Keyword.put(type_constraints, :__union_tag__, union_tag)
else
type_constraints
end
result = result =
Ash.Type.handle_change( Ash.Type.handle_change(
{:array, type}, {:array, type},
old_values_by_type[name] || [], old_values_by_type[name] || [],
new_values, new_values,
Keyword.put(constraints, :items, constraints[:types][name][:constraints]) Keyword.put(constraints, :items, type_constraints)
) )
case result do case result do

View file

@ -283,7 +283,8 @@ defmodule Ash.Test.CodeInterfaceTest do
assert bob.first_name == "bob_updated" assert bob.first_name == "bob_updated"
assert_received {:notification, %Ash.Notifier.Notification{} = notification} assert_received {:notification,
%Ash.Notifier.Notification{resource: User, action: %{name: :update}}}
end end
test "bulk update can take a @context options" do test "bulk update can take a @context options" do

View file

@ -19,8 +19,12 @@ defmodule Ash.Test.UUIDv7Test do
test "bingenerate/1 is ordered" do test "bingenerate/1 is ordered" do
uuids = uuids =
for _ <- 1..10_000 do for _ <- 1..100 do
Ash.UUIDv7.bingenerate() uuid = Ash.UUIDv7.bingenerate()
# only guaranteed sorted if >= 1 nanosecond apart
# can't sleep for one nanoseond AFAIK, so sleep for 1 ms
:timer.sleep(1)
uuid
end end
assert uuids == Enum.sort(uuids) assert uuids == Enum.sort(uuids)