mirror of
https://github.com/ash-project/ash.git
synced 2024-09-19 21:13:10 +12:00
improvement: support parent/1
in relationships
This commit is contained in:
parent
b9abe2aa72
commit
bda7c56543
17 changed files with 625 additions and 171 deletions
|
@ -125,7 +125,7 @@
|
|||
{Credo.Check.Refactor.MatchInCondition, false},
|
||||
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
|
||||
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
|
||||
{Credo.Check.Refactor.Nesting, [max_nesting: 6]},
|
||||
{Credo.Check.Refactor.Nesting, [max_nesting: 7]},
|
||||
{Credo.Check.Refactor.UnlessWithElse, []},
|
||||
{Credo.Check.Refactor.WithClauses, []},
|
||||
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
erlang 26.0.1
|
||||
elixir 1.15.0
|
||||
erlang 26.0.2
|
||||
elixir 1.15.4
|
||||
|
|
|
@ -52,7 +52,7 @@ The following functions are built in:
|
|||
|
||||
- `exists/2` | `exists(foo.bar, name == "fred")` takes an expression scoped to the destination resource, and checks if any related entry matches. See the section on `exists` below.
|
||||
- `path.exists/2` | Same as `exists` but the source of the relationship is itself a nested relationship. See the section on `exists` below.
|
||||
- `parent/1` | Allows an expression scoped to a resource to refer to the "outer" context.
|
||||
- `parent/1` | Allows an expression scoped to a resource to refer to the "outer" context. Used in relationship filters and `exists`
|
||||
|
||||
## DateTime Functions
|
||||
|
||||
|
@ -151,6 +151,19 @@ Ash.Query.filter(Post, author.exists(roles, name == :admin) and author.active)
|
|||
|
||||
While the above is not common, it can be useful in some specific circumstances, and is used under the hood by the policy authorizer when combining the filters of various resources to create a single filter.
|
||||
|
||||
## Relationship Filters
|
||||
|
||||
When filtering relationships, you can use the `parent/1` function to scope a part of the expression to "source" of the join. This allows for very expressive relationships! Keep in mind, however, that if you want to update and/or manage these relationships, you'll have to make sure that any attributes that make these things actually related are properly set.
|
||||
|
||||
```elixir
|
||||
has_many :descendents, __MODULE__ do
|
||||
description "All descendents in the same tree"
|
||||
no_attributes? true # this says that there is no matching source_attribute and destination_attribute on this relationship
|
||||
# This is an example using postgres' ltree extension.
|
||||
filter expr(tree_id == parent(tree_id) and fragment("? @> ?", parent(path), path))
|
||||
end
|
||||
```
|
||||
|
||||
## Portability
|
||||
|
||||
Ash expressions being portable is more important than it sounds. For example, if you were using AshPostgres and had the following calculation, which is an expression capable of being run in elixir or translated to SQL:
|
||||
|
|
|
@ -142,7 +142,7 @@ defmodule Ash.Actions.Load do
|
|||
|
||||
defp attach_to_many_loads(
|
||||
value,
|
||||
%{name: name, no_attributes?: true},
|
||||
%{name: name, no_attributes?: true} = relationship,
|
||||
data,
|
||||
lead_path
|
||||
) do
|
||||
|
@ -158,9 +158,55 @@ defmodule Ash.Actions.Load do
|
|||
value |> Map.values() |> List.flatten()
|
||||
end
|
||||
|
||||
map_or_update(data, lead_path, fn record ->
|
||||
Map.put(record, name, value)
|
||||
end)
|
||||
if has_parent_expr?(relationship) do
|
||||
case relationship_type_with_non_simple_equality(relationship) do
|
||||
:error ->
|
||||
map_or_update(data, lead_path, fn record ->
|
||||
source_value = Map.get(record, relationship.source_attribute)
|
||||
|
||||
Map.put(
|
||||
record,
|
||||
name,
|
||||
Enum.filter(value, fn
|
||||
%{__lateral_join_source__: destination_value}
|
||||
when not is_nil(destination_value) ->
|
||||
destination_value == source_value
|
||||
|
||||
_ ->
|
||||
# handle backwards compatibility on join
|
||||
# data layers should now all be setting `__lateral_join_source__` for any
|
||||
# lateral joins
|
||||
true
|
||||
end)
|
||||
)
|
||||
end)
|
||||
|
||||
{:ok, type} ->
|
||||
map_or_update(data, lead_path, fn record ->
|
||||
source_value = Map.get(record, relationship.source_attribute)
|
||||
|
||||
Map.put(
|
||||
record,
|
||||
name,
|
||||
Enum.filter(value, fn
|
||||
%{__lateral_join_source__: destination_value}
|
||||
when not is_nil(destination_value) ->
|
||||
Ash.Type.equal?(type, destination_value, source_value)
|
||||
|
||||
_ ->
|
||||
# handle backwards compatibility on join
|
||||
# data layers should now all be setting `__lateral_join_source__` for any
|
||||
# lateral joins
|
||||
true
|
||||
end)
|
||||
)
|
||||
end)
|
||||
end
|
||||
else
|
||||
map_or_update(data, lead_path, fn record ->
|
||||
Map.put(record, name, value)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp attach_to_many_loads(value, last_relationship, data, lead_path)
|
||||
|
@ -222,43 +268,75 @@ defmodule Ash.Actions.Load do
|
|||
end
|
||||
|
||||
defp attach_to_many_loads(value, last_relationship, data, lead_path) do
|
||||
if Ash.Resource.Info.primary_key_simple_equality?(last_relationship.destination) do
|
||||
values = Enum.group_by(value, &Map.get(&1, last_relationship.destination_attribute))
|
||||
case relationship_type_with_non_simple_equality(last_relationship) do
|
||||
:error ->
|
||||
values =
|
||||
Enum.group_by(value, fn
|
||||
%{__lateral_join_source__: lateral_join_source}
|
||||
when not is_nil(lateral_join_source) ->
|
||||
lateral_join_source
|
||||
|
||||
map_or_update(data, lead_path, fn record ->
|
||||
source_key = Map.get(record, last_relationship.source_attribute)
|
||||
related_records = Map.get(values, source_key, [])
|
||||
Map.put(record, last_relationship.name, related_records)
|
||||
end)
|
||||
else
|
||||
destination_attribute = last_relationship.destination_attribute
|
||||
|
||||
type =
|
||||
Ash.Resource.Info.attribute(last_relationship.destination, destination_attribute).type
|
||||
|
||||
map_or_update(data, lead_path, fn record ->
|
||||
source_key = Map.get(record, last_relationship.source_attribute)
|
||||
|
||||
related_records =
|
||||
Enum.filter(value, fn maybe_related ->
|
||||
Ash.Type.equal?(
|
||||
type,
|
||||
Map.get(maybe_related, destination_attribute),
|
||||
source_key
|
||||
)
|
||||
record ->
|
||||
Map.get(record, last_relationship.destination_attribute)
|
||||
end)
|
||||
|
||||
Map.put(record, last_relationship.name, related_records)
|
||||
end)
|
||||
map_or_update(data, lead_path, fn record ->
|
||||
source_key = Map.get(record, last_relationship.source_attribute)
|
||||
related_records = Map.get(values, source_key, [])
|
||||
Map.put(record, last_relationship.name, related_records)
|
||||
end)
|
||||
|
||||
{:ok, type} ->
|
||||
destination_attribute = last_relationship.destination_attribute
|
||||
|
||||
map_or_update(data, lead_path, fn record ->
|
||||
source_key = Map.get(record, last_relationship.source_attribute)
|
||||
|
||||
related_records =
|
||||
Enum.filter(value, fn
|
||||
%{__lateral_join_source__: lateral_join_source}
|
||||
when not is_nil(lateral_join_source) ->
|
||||
lateral_join_source
|
||||
|
||||
maybe_related ->
|
||||
Ash.Type.equal?(
|
||||
type,
|
||||
Map.get(maybe_related, destination_attribute),
|
||||
source_key
|
||||
)
|
||||
end)
|
||||
|
||||
Map.put(record, last_relationship.name, related_records)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp attach_to_one_loads(value, %{name: name, no_attributes?: true}, data, lead_path) do
|
||||
defp attach_to_one_loads(
|
||||
value,
|
||||
%{name: name, no_attributes?: true} = relationship,
|
||||
data,
|
||||
lead_path
|
||||
) do
|
||||
map_or_update(data, lead_path, fn record ->
|
||||
if is_map(data) do
|
||||
Map.put(record, name, value |> List.wrap() |> Enum.at(0) |> elem(1))
|
||||
else
|
||||
Map.put(record, name, value |> List.wrap() |> Enum.at(0))
|
||||
source_value = Map.get(record, relationship.source_attribute)
|
||||
|
||||
case relationship_type_with_non_simple_equality(relationship) do
|
||||
{:ok, type} ->
|
||||
case value do
|
||||
%{__lateral_join_source__: destination_value} when not is_nil(destination_value) ->
|
||||
Ash.Type.equal?(
|
||||
type,
|
||||
source_value,
|
||||
destination_value
|
||||
)
|
||||
|
||||
value ->
|
||||
Map.put(record, name, value |> List.wrap() |> Enum.at(0))
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
@ -324,20 +402,35 @@ defmodule Ash.Actions.Load do
|
|||
Map.put(record, last_relationship.name, related_record)
|
||||
end)
|
||||
else
|
||||
map_or_update(data, lead_path, fn record ->
|
||||
source_key = Map.get(record, last_relationship.source_attribute)
|
||||
case relationship_type_with_non_simple_equality(last_relationship) do
|
||||
:error ->
|
||||
map_or_update(data, lead_path, fn record ->
|
||||
source_key = Map.get(record, last_relationship.source_attribute)
|
||||
|
||||
related_record =
|
||||
Enum.find(value, fn maybe_related ->
|
||||
Ash.Type.equal?(
|
||||
type,
|
||||
Map.get(maybe_related, destination_attribute),
|
||||
source_key
|
||||
)
|
||||
related_record =
|
||||
Enum.find(value, fn maybe_related ->
|
||||
Map.get(maybe_related, destination_attribute) == source_key
|
||||
end)
|
||||
|
||||
Map.put(record, last_relationship.name, related_record)
|
||||
end)
|
||||
|
||||
Map.put(record, last_relationship.name, related_record)
|
||||
end)
|
||||
{:ok, type} ->
|
||||
map_or_update(data, lead_path, fn record ->
|
||||
source_key = Map.get(record, last_relationship.source_attribute)
|
||||
|
||||
related_record =
|
||||
Enum.find(value, fn maybe_related ->
|
||||
Ash.Type.equal?(
|
||||
type,
|
||||
Map.get(maybe_related, destination_attribute),
|
||||
source_key
|
||||
)
|
||||
end)
|
||||
|
||||
Map.put(record, last_relationship.name, related_record)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -349,53 +442,75 @@ defmodule Ash.Actions.Load do
|
|||
|> Map.get(join_path, %{})
|
||||
|> Map.get(:data, [])
|
||||
|
||||
source_attribute_on_join_resource_type =
|
||||
Ash.Resource.Info.attribute(
|
||||
last_relationship.through,
|
||||
last_relationship.source_attribute_on_join_resource
|
||||
).type
|
||||
|
||||
destination_attribute_on_join_resource_type =
|
||||
Ash.Resource.Info.attribute(
|
||||
last_relationship.through,
|
||||
last_relationship.destination_attribute_on_join_resource
|
||||
).type
|
||||
join_relationship =
|
||||
Ash.Resource.Info.relationship(
|
||||
last_relationship.source,
|
||||
last_relationship.join_relationship
|
||||
)
|
||||
|
||||
map_or_update(data, lead_path, fn record ->
|
||||
source_value = Map.get(record, last_relationship.source_attribute)
|
||||
|
||||
join_values =
|
||||
join_data
|
||||
|> Enum.filter(fn join_row ->
|
||||
Ash.Type.equal?(
|
||||
source_attribute_on_join_resource_type,
|
||||
Map.get(join_row, last_relationship.source_attribute_on_join_resource),
|
||||
source_value
|
||||
)
|
||||
end)
|
||||
|> Enum.map(&Map.get(&1, last_relationship.destination_attribute_on_join_resource))
|
||||
|
||||
related_records =
|
||||
value
|
||||
|> Enum.filter(fn
|
||||
%{__lateral_join_source__: join_value} ->
|
||||
Ash.Type.equal?(
|
||||
destination_attribute_on_join_resource_type,
|
||||
source_value,
|
||||
join_value
|
||||
)
|
||||
|
||||
value ->
|
||||
destination_value = Map.get(value, last_relationship.destination_attribute)
|
||||
|
||||
Enum.any?(join_values, fn join_value ->
|
||||
case relationship_type_with_non_simple_equality(join_relationship) do
|
||||
{:ok, type} ->
|
||||
join_data
|
||||
|> Enum.filter(fn join_row ->
|
||||
Ash.Type.equal?(
|
||||
destination_attribute_on_join_resource_type,
|
||||
destination_value,
|
||||
join_value
|
||||
type,
|
||||
Map.get(join_row, last_relationship.source_attribute_on_join_resource),
|
||||
source_value
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|> Enum.map(&Map.get(&1, last_relationship.destination_attribute_on_join_resource))
|
||||
|
||||
:error ->
|
||||
join_data
|
||||
|> Enum.filter(fn join_row ->
|
||||
Map.get(join_row, last_relationship.source_attribute_on_join_resource) ==
|
||||
source_value
|
||||
end)
|
||||
|> Enum.map(&Map.get(&1, last_relationship.destination_attribute_on_join_resource))
|
||||
end
|
||||
|
||||
related_records =
|
||||
case relationship_type_with_non_simple_equality(last_relationship) do
|
||||
{:ok, type} ->
|
||||
value
|
||||
|> Enum.filter(fn
|
||||
%{__lateral_join_source__: join_value} when not is_nil(join_value) ->
|
||||
Ash.Type.equal?(
|
||||
type,
|
||||
source_value,
|
||||
join_value
|
||||
)
|
||||
|
||||
value ->
|
||||
destination_value = Map.get(value, last_relationship.destination_attribute)
|
||||
|
||||
Enum.any?(join_values, fn join_value ->
|
||||
Ash.Type.equal?(
|
||||
type,
|
||||
destination_value,
|
||||
join_value
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
||||
:error ->
|
||||
value
|
||||
|> Enum.filter(fn
|
||||
%{__lateral_join_source__: join_value} when not is_nil(join_value) ->
|
||||
source_value == join_value
|
||||
|
||||
value ->
|
||||
destination_value = Map.get(value, last_relationship.destination_attribute)
|
||||
|
||||
Enum.any?(join_values, fn join_value ->
|
||||
join_value == destination_value
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
Map.put(record, last_relationship.name, related_records)
|
||||
end)
|
||||
|
@ -1297,7 +1412,7 @@ defmodule Ash.Actions.Load do
|
|||
|
||||
authorization_filter ->
|
||||
base_query
|
||||
|> Ash.Query.do_filter(authorization_filter)
|
||||
|> Ash.Query.do_filter(authorization_filter, parent_stack: relationship.source)
|
||||
end
|
||||
|
||||
source_data = get_in(data, parent_data_path)
|
||||
|
@ -1388,6 +1503,7 @@ defmodule Ash.Actions.Load do
|
|||
end
|
||||
|
||||
if action.manual do
|
||||
raise_if_parent_expr!(relationship, "manual actions")
|
||||
false
|
||||
else
|
||||
{offset, limit} = offset_and_limit(query)
|
||||
|
@ -1398,29 +1514,72 @@ defmodule Ash.Actions.Load do
|
|||
|
||||
cond do
|
||||
is_many_to_many_not_unique_on_join?(relationship) ->
|
||||
raise_if_parent_expr!(
|
||||
relationship,
|
||||
"many to many relationships that don't have unique constraints on their join resource attributes"
|
||||
)
|
||||
|
||||
false
|
||||
|
||||
limit == 1 && is_nil(relationship.context) && is_nil(relationship.filter) &&
|
||||
is_nil(relationship.sort) && relationship.type != :many_to_many ->
|
||||
false
|
||||
has_parent_expr?(relationship)
|
||||
|
||||
limit == 1 && (source_data == :unknown || Enum.count_until(source_data, 2) == 1) &&
|
||||
relationship.type != :many_to_many ->
|
||||
false
|
||||
has_parent_expr?(relationship)
|
||||
|
||||
true ->
|
||||
lateral_join =
|
||||
(limit || offset || relationship.type == :many_to_many) &&
|
||||
Ash.DataLayer.data_layer_can?(
|
||||
relationship.source,
|
||||
{:lateral_join, resources}
|
||||
)
|
||||
if has_parent_expr?(relationship) do
|
||||
true
|
||||
else
|
||||
lateral_join =
|
||||
(limit || offset || relationship.type == :many_to_many) &&
|
||||
Ash.DataLayer.data_layer_can?(
|
||||
relationship.source,
|
||||
{:lateral_join, resources}
|
||||
)
|
||||
|
||||
!!lateral_join
|
||||
Ash.DataLayer.prefer_lateral_join_for_many_to_many?(
|
||||
Ash.DataLayer.data_layer(relationship.source)
|
||||
) and !!lateral_join
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp raise_if_parent_expr!(relationship, reason) do
|
||||
if has_parent_expr?(relationship) do
|
||||
raise ArgumentError, "Found `parent_expr` in unsupported context: #{reason}"
|
||||
end
|
||||
end
|
||||
|
||||
defp has_parent_expr?(%{filter: filter}, depth \\ 0) do
|
||||
not is_nil(
|
||||
Ash.Filter.find(filter, fn
|
||||
%Ash.Query.Call{name: :parent, args: [expr]} ->
|
||||
if depth == 0 do
|
||||
true
|
||||
else
|
||||
has_parent_expr?(expr, depth - 1)
|
||||
end
|
||||
|
||||
%Ash.Query.Exists{expr: expr} ->
|
||||
has_parent_expr?(expr, depth + 1)
|
||||
|
||||
%Ash.Query.Parent{expr: expr} ->
|
||||
if depth == 0 do
|
||||
true
|
||||
else
|
||||
has_parent_expr?(expr, depth - 1)
|
||||
end
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
defp is_many_to_many_not_unique_on_join?(%{type: :many_to_many} = relationship) do
|
||||
join_keys =
|
||||
Enum.sort([
|
||||
|
@ -1510,12 +1669,13 @@ defmodule Ash.Actions.Load do
|
|||
}
|
||||
})
|
||||
|> Ash.Query.set_context(relationship.context)
|
||||
|> Ash.Query.do_filter(relationship.filter)
|
||||
|> Ash.Query.do_filter(relationship.filter, parent_stack: relationship.source)
|
||||
|> Ash.Query.sort(relationship.sort, prepend?: true)
|
||||
|> remove_relationships_from_load()
|
||||
|> read(relationship.read_action, request_opts)
|
||||
|
||||
lateral_join?(query, relationship, source_data) && (limit || offset) ->
|
||||
lateral_join?(query, relationship, source_data) &&
|
||||
(limit || offset || has_parent_expr?(relationship)) ->
|
||||
query
|
||||
|> Ash.Query.set_context(%{
|
||||
data_layer: %{
|
||||
|
@ -1528,7 +1688,7 @@ defmodule Ash.Actions.Load do
|
|||
}
|
||||
})
|
||||
|> Ash.Query.set_context(relationship.context)
|
||||
|> Ash.Query.do_filter(relationship.filter)
|
||||
|> Ash.Query.do_filter(relationship.filter, parent_stack: relationship.source)
|
||||
|> Ash.Query.sort(relationship.sort, prepend?: true)
|
||||
|> remove_relationships_from_load()
|
||||
|> read(relationship.read_action, request_opts)
|
||||
|
@ -1549,7 +1709,7 @@ defmodule Ash.Actions.Load do
|
|||
true ->
|
||||
query
|
||||
|> Ash.Query.set_context(relationship.context)
|
||||
|> Ash.Query.do_filter(relationship.filter)
|
||||
|> Ash.Query.do_filter(relationship.filter, parent_stack: relationship.source)
|
||||
|> Ash.Query.sort(relationship.sort, prepend?: true)
|
||||
|> remove_relationships_from_load()
|
||||
|> read(
|
||||
|
@ -1582,7 +1742,7 @@ defmodule Ash.Actions.Load do
|
|||
) do
|
||||
query
|
||||
|> Ash.Query.set_context(relationship.context)
|
||||
|> Ash.Query.do_filter(relationship.filter)
|
||||
|> Ash.Query.do_filter(relationship.filter, parent_stack: relationship.source)
|
||||
|> Ash.Query.sort(relationship.sort, prepend?: true)
|
||||
|> remove_relationships_from_load()
|
||||
|> read(relationship.read_action, request_opts)
|
||||
|
@ -1594,11 +1754,14 @@ defmodule Ash.Actions.Load do
|
|||
|
||||
join_data = get_in(data, join_request_path ++ [:data])
|
||||
|
||||
destination_attribute_on_join_resource_type =
|
||||
Ash.Resource.Info.attribute(
|
||||
relationship.through,
|
||||
relationship.destination_attribute_on_join_resource
|
||||
).type
|
||||
equality =
|
||||
case relationship_type_with_non_simple_equality(relationship) do
|
||||
{:ok, type} ->
|
||||
&equal_by_type(type, &1, &2)
|
||||
|
||||
:error ->
|
||||
&(&1 == &2)
|
||||
end
|
||||
|
||||
join_data
|
||||
|> Enum.uniq_by(
|
||||
|
@ -1612,8 +1775,7 @@ defmodule Ash.Actions.Load do
|
|||
group =
|
||||
Enum.map(group, fn join_row ->
|
||||
Enum.find_value(results, fn {record, index} ->
|
||||
if Ash.Type.equal?(
|
||||
destination_attribute_on_join_resource_type,
|
||||
if equality.(
|
||||
Map.get(join_row, relationship.destination_attribute_on_join_resource),
|
||||
Map.get(record, relationship.destination_attribute)
|
||||
) do
|
||||
|
@ -1639,6 +1801,7 @@ defmodule Ash.Actions.Load do
|
|||
end
|
||||
end)
|
||||
else
|
||||
# TODO: This needs to support `Ash.Type.equal?`
|
||||
results
|
||||
|> Enum.with_index()
|
||||
|> Enum.group_by(fn {record, _i} ->
|
||||
|
@ -1689,6 +1852,31 @@ defmodule Ash.Actions.Load do
|
|||
end
|
||||
end
|
||||
|
||||
defp relationship_type_with_non_simple_equality(relationship) do
|
||||
with :error <-
|
||||
type_without_simple_equality(relationship.source, relationship.source_attribute) do
|
||||
type_without_simple_equality(relationship.destination, relationship.destination_attribute)
|
||||
end
|
||||
end
|
||||
|
||||
defp type_without_simple_equality(resource, attribute) do
|
||||
case Ash.Resource.Info.attribute(resource, attribute) do
|
||||
%{type: type} ->
|
||||
if Ash.Type.simple_equality?(type) do
|
||||
:error
|
||||
else
|
||||
{:ok, type}
|
||||
end
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp equal_by_type(type, l, r) do
|
||||
Ash.Type.equal?(type, l, r)
|
||||
end
|
||||
|
||||
defp remove_relationships_from_load(query) do
|
||||
case query.load do
|
||||
empty when empty in [nil, []] ->
|
||||
|
|
|
@ -238,7 +238,9 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
authorize?: opts[:authorize?]
|
||||
)
|
||||
|> Ash.Query.filter(^keys)
|
||||
|> Ash.Query.do_filter(relationship.filter)
|
||||
|> Ash.Query.do_filter(relationship.filter,
|
||||
parent_stack: relationship.source
|
||||
)
|
||||
|> Ash.Query.sort(relationship.sort, prepend?: true)
|
||||
|> Ash.Query.set_context(relationship.context)
|
||||
|> Ash.Query.set_tenant(changeset.tenant)
|
||||
|
@ -879,7 +881,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
})
|
||||
|> Ash.Query.for_read(read, input, actor: actor, authorize?: opts[:authorize?])
|
||||
|> Ash.Query.filter(^keys)
|
||||
|> Ash.Query.do_filter(relationship.filter)
|
||||
|> Ash.Query.do_filter(relationship.filter, parent_stack: relationship.source)
|
||||
|> Ash.Query.sort(relationship.sort, prepend?: true)
|
||||
|> Ash.Query.set_context(relationship.context)
|
||||
|> Ash.Query.set_tenant(changeset.tenant)
|
||||
|
@ -1530,7 +1532,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
|> Ash.Query.limit(1)
|
||||
|> Ash.Query.set_tenant(changeset.tenant)
|
||||
|> Ash.Query.set_context(join_relationship.context)
|
||||
|> Ash.Query.do_filter(relationship.filter)
|
||||
|> Ash.Query.do_filter(relationship.filter, parent_stack: relationship.source)
|
||||
|> Ash.Query.sort(relationship.sort, prepend?: true)
|
||||
|> api(changeset, join_relationship).read_one(
|
||||
authorize?: opts[:authorize?],
|
||||
|
|
|
@ -1362,11 +1362,7 @@ defmodule Ash.Actions.Read do
|
|||
calculations_in_query
|
||||
),
|
||||
{:ok, query} <-
|
||||
Ash.DataLayer.filter(
|
||||
query,
|
||||
filter,
|
||||
ash_query.resource
|
||||
),
|
||||
data_layer_filter(query, filter, ash_query),
|
||||
{:ok, query} <-
|
||||
Ash.DataLayer.sort(query, ash_query.sort, ash_query.resource),
|
||||
{:ok, query} <-
|
||||
|
@ -1444,6 +1440,14 @@ defmodule Ash.Actions.Read do
|
|||
)
|
||||
end
|
||||
|
||||
defp data_layer_filter(query, filter, ash_query) do
|
||||
Ash.DataLayer.filter(
|
||||
query,
|
||||
filter,
|
||||
ash_query.resource
|
||||
)
|
||||
end
|
||||
|
||||
defp loaded_query(query, calculations_at_runtime) do
|
||||
query
|
||||
|> load_calc_requirements(calculations_at_runtime)
|
||||
|
|
|
@ -85,6 +85,7 @@ defmodule Ash.DataLayer do
|
|||
{:ok, data_layer_query()} | {:error, term}
|
||||
@callback distinct(data_layer_query(), list(atom), resource :: Ash.Resource.t()) ::
|
||||
{:ok, data_layer_query()} | {:error, term}
|
||||
@callback prefer_lateral_join_for_many_to_many?() :: boolean
|
||||
@callback limit(
|
||||
data_layer_query(),
|
||||
limit :: non_neg_integer(),
|
||||
|
@ -217,6 +218,7 @@ defmodule Ash.DataLayer do
|
|||
upsert: 3,
|
||||
functions: 1,
|
||||
in_transaction?: 1,
|
||||
prefer_lateral_join_for_many_to_many?: 0,
|
||||
add_aggregate: 3,
|
||||
add_aggregates: 3,
|
||||
add_calculation: 4,
|
||||
|
@ -232,6 +234,16 @@ defmodule Ash.DataLayer do
|
|||
Extension.get_persisted(resource, :data_layer)
|
||||
end
|
||||
|
||||
@doc "Wether or not lateral joins should be used for many to many relationships by default"
|
||||
@spec prefer_lateral_join_for_many_to_many?(Ash.DataLayer.t()) :: boolean
|
||||
def prefer_lateral_join_for_many_to_many?(data_layer) do
|
||||
if function_exported?(data_layer, :prefer_lateral_join_for_many_to_many?, 0) do
|
||||
data_layer.prefer_lateral_join_for_many_to_many?()
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Whether or not the data layer supports a specific feature"
|
||||
@spec data_layer_can?(Ash.Resource.t() | Spark.Dsl.t(), Ash.DataLayer.feature()) :: boolean
|
||||
def data_layer_can?(resource, feature) do
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
defmodule Ash.DataLayer.Ets do
|
||||
@behaviour Ash.DataLayer
|
||||
require Ash.Query
|
||||
|
||||
@ets %Spark.Dsl.Section{
|
||||
name: :ets,
|
||||
|
@ -188,11 +189,9 @@ defmodule Ash.DataLayer.Ets do
|
|||
|
||||
@doc false
|
||||
@impl true
|
||||
def can?(resource, :async_engine) do
|
||||
not private?(resource)
|
||||
end
|
||||
|
||||
def can?(_, :distinct_sort), do: true
|
||||
def can?(resource, :async_engine), do: not private?(resource)
|
||||
def can?(_, {:lateral_join, _}), do: true
|
||||
def can?(_, :bulk_create), do: true
|
||||
def can?(_, :composite_primary_key), do: true
|
||||
def can?(_, :expression_calculation), do: true
|
||||
|
@ -360,11 +359,12 @@ defmodule Ash.DataLayer.Ets do
|
|||
aggregates: aggregates,
|
||||
api: api
|
||||
},
|
||||
_resource
|
||||
_resource,
|
||||
parent \\ nil
|
||||
) do
|
||||
with {:ok, records} <- get_records(resource, tenant),
|
||||
{:ok, records} <-
|
||||
filter_matches(records, filter, api),
|
||||
filter_matches(records, filter, api, parent),
|
||||
records <- Sort.runtime_sort(records, distinct_sort || sort, api: api),
|
||||
records <- Sort.runtime_distinct(records, distinct, api: api),
|
||||
records <- Sort.runtime_sort(records, sort, api: api),
|
||||
|
@ -383,6 +383,151 @@ defmodule Ash.DataLayer.Ets do
|
|||
defp do_limit(records, nil), do: records
|
||||
defp do_limit(records, limit), do: Enum.take(records, limit)
|
||||
|
||||
@impl true
|
||||
def prefer_lateral_join_for_many_to_many?, do: false
|
||||
|
||||
@impl true
|
||||
def run_query_with_lateral_join(
|
||||
query,
|
||||
root_data,
|
||||
_destination_resource,
|
||||
[{source_query, source_attribute, destination_attribute, relationship}]
|
||||
) do
|
||||
source_attributes = Enum.map(root_data, &Map.get(&1, source_attribute))
|
||||
|
||||
source_query
|
||||
|> Ash.Query.filter(ref(^source_attribute) in ^source_attributes)
|
||||
|> Ash.Query.set_context(%{private: %{internal?: true}})
|
||||
|> Ash.Query.unset(:load)
|
||||
|> Ash.Query.unset(:select)
|
||||
|> query.api.read(authorize?: false)
|
||||
|> case do
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
{:ok, root_data} ->
|
||||
root_data
|
||||
|> Enum.reduce_while({:ok, []}, fn parent, {:ok, results} ->
|
||||
new_filter =
|
||||
if Map.get(relationship, :no_attributes?) do
|
||||
query.filter
|
||||
else
|
||||
Ash.Filter.add_to_filter(query.filter, [
|
||||
{destination_attribute, Map.get(parent, source_attribute)}
|
||||
])
|
||||
end
|
||||
|
||||
query = %{query | filter: new_filter}
|
||||
|
||||
case run_query(query, relationship.source, parent) do
|
||||
{:ok, new_results} ->
|
||||
new_results =
|
||||
Enum.map(
|
||||
new_results,
|
||||
&Map.put(&1, :__lateral_join_source__, Map.get(parent, source_attribute))
|
||||
)
|
||||
|
||||
{:cont, {:ok, new_results ++ results}}
|
||||
|
||||
{:error, error} ->
|
||||
{:halt, {:error, error}}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{:ok, results} ->
|
||||
{:ok, results}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def run_query_with_lateral_join(query, root_data, _destination_resource, [
|
||||
{source_query, source_attribute, source_attribute_on_join_resource, relationship},
|
||||
{through_query, destination_attribute_on_join_resource, destination_attribute,
|
||||
_through_relationship}
|
||||
]) do
|
||||
source_attributes = Enum.map(root_data, &Map.get(&1, source_attribute))
|
||||
|
||||
source_query
|
||||
|> Ash.Query.unset(:load)
|
||||
|> Ash.Query.filter(ref(^source_attribute) in ^source_attributes)
|
||||
|> Ash.Query.set_context(%{private: %{internal?: true}})
|
||||
|> query.api.read(authorize?: false)
|
||||
|> case do
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
{:ok, root_data} ->
|
||||
root_data
|
||||
|> Enum.reduce_while({:ok, []}, fn parent, {:ok, results} ->
|
||||
through_query
|
||||
|> Ash.Query.filter(
|
||||
ref(^source_attribute_on_join_resource) ==
|
||||
^Map.get(parent, source_attribute)
|
||||
)
|
||||
|> Ash.Query.set_context(%{private: %{internal?: true}})
|
||||
|> query.api.read(authorize?: false)
|
||||
|> case do
|
||||
{:ok, join_data} ->
|
||||
join_attrs =
|
||||
Enum.map(join_data, &Map.get(&1, destination_attribute_on_join_resource))
|
||||
|
||||
new_filter =
|
||||
if is_nil(query.filter) do
|
||||
Ash.Filter.parse!(query.resource, [
|
||||
{destination_attribute, [in: join_attrs]}
|
||||
])
|
||||
else
|
||||
Ash.Filter.add_to_filter(query.filter, [
|
||||
{destination_attribute, [in: join_attrs]}
|
||||
])
|
||||
end
|
||||
|
||||
query = %{query | filter: new_filter}
|
||||
|
||||
case run_query(query, relationship.source, parent) do
|
||||
{:ok, new_results} ->
|
||||
new_results =
|
||||
Enum.flat_map(new_results, fn result ->
|
||||
join_data
|
||||
|> Enum.flat_map(fn join_row ->
|
||||
if Map.get(join_row, destination_attribute_on_join_resource) ==
|
||||
Map.get(result, destination_attribute) do
|
||||
[
|
||||
Map.put(
|
||||
result,
|
||||
:__lateral_join_source__,
|
||||
Map.get(join_row, source_attribute_on_join_resource)
|
||||
)
|
||||
]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
{:cont, {:ok, new_results ++ results}}
|
||||
|
||||
{:error, error} ->
|
||||
{:halt, {:error, error}}
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
{:halt, {:error, error}}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{:ok, results} ->
|
||||
{:ok, results}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def do_add_calculations(records, _resource, [], _api), do: {:ok, records}
|
||||
|
||||
def do_add_calculations(records, resource, calculations, api) do
|
||||
|
@ -679,10 +824,11 @@ defmodule Ash.DataLayer.Ets do
|
|||
end
|
||||
end
|
||||
|
||||
defp filter_matches(records, nil, _api), do: {:ok, records}
|
||||
defp filter_matches(records, filter, api, parent \\ nil)
|
||||
defp filter_matches(records, nil, _api, _parent), do: {:ok, records}
|
||||
|
||||
defp filter_matches(records, filter, api) do
|
||||
Ash.Filter.Runtime.filter_matches(api, records, filter)
|
||||
defp filter_matches(records, filter, api, parent) do
|
||||
Ash.Filter.Runtime.filter_matches(api, records, filter, parent: parent)
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
|
|
@ -1126,7 +1126,7 @@ defmodule Ash.Filter do
|
|||
relationship.destination
|
||||
|> Ash.Query.set_context(relationship.context)
|
||||
|> Ash.Query.sort(relationship.sort, prepend?: true)
|
||||
|> Ash.Query.do_filter(relationship.filter)
|
||||
|> Ash.Query.do_filter(relationship.filter, parent_stack: relationship.source)
|
||||
|> Ash.Query.for_read(action, %{},
|
||||
actor: actor,
|
||||
authorize?: true,
|
||||
|
@ -1664,7 +1664,7 @@ defmodule Ash.Filter do
|
|||
relationship.destination
|
||||
|> Ash.Query.new(api)
|
||||
|> Ash.Query.do_filter(filter)
|
||||
|> Ash.Query.do_filter(relationship.filter)
|
||||
|> Ash.Query.do_filter(relationship.filter, parent_stack: relationship.source)
|
||||
|> Ash.Query.sort(relationship.sort, prepend?: true)
|
||||
|> Ash.Query.set_context(relationship.context)
|
||||
|> filter_related_in(relationship, :lists.droplast(path), api, data)
|
||||
|
@ -2724,6 +2724,13 @@ defmodule Ash.Filter do
|
|||
end
|
||||
end
|
||||
|
||||
defp resolve_call(
|
||||
%Call{name: :parent, args: [arg], relationship_path: []},
|
||||
context
|
||||
) do
|
||||
do_hydrate_refs(%Ash.Query.Parent{expr: arg}, context)
|
||||
end
|
||||
|
||||
defp resolve_call(%Call{name: name, args: args} = call, context) do
|
||||
could_be_calculation? = Enum.count(args) == 1 && Keyword.keyword?(Enum.at(args, 0))
|
||||
|
||||
|
@ -3017,6 +3024,10 @@ defmodule Ash.Filter do
|
|||
end
|
||||
|
||||
def do_hydrate_refs(%Ash.Query.Parent{expr: expr} = this, context) do
|
||||
if !Map.has_key?(context, :parent_stack) || context.parent_stack in [[], nil] do
|
||||
raise "Attempted to use parent expression without a known parent: #{inspect(this)}"
|
||||
end
|
||||
|
||||
context =
|
||||
%{
|
||||
context
|
||||
|
@ -3173,25 +3184,14 @@ defmodule Ash.Filter do
|
|||
%{__function__?: true, arguments: args} = func ->
|
||||
%{func | arguments: Enum.map(args, &move_to_relationship_path(&1, relationship_path))}
|
||||
|
||||
%Ash.Query.Exists{expr: expr} = exists ->
|
||||
%{
|
||||
exists
|
||||
| at_path: relationship_path ++ exists.at_path,
|
||||
expr:
|
||||
map(expr, fn
|
||||
%Ash.Query.Parent{} = this ->
|
||||
move_to_relationship_path(this, relationship_path)
|
||||
%Ash.Query.Exists{} = exists ->
|
||||
%{exists | at_path: relationship_path ++ exists.at_path}
|
||||
|
||||
other ->
|
||||
other
|
||||
end)
|
||||
}
|
||||
# %Ash.Query.Parent{expr: expr} = this ->
|
||||
# %{this | expr: move_to_relationship_path(expr, relationship_path)}
|
||||
|
||||
%Ash.Query.Parent{expr: expr} = this ->
|
||||
%{this | expr: move_to_relationship_path(expr, relationship_path)}
|
||||
|
||||
%Call{args: args} = call ->
|
||||
%{call | args: Enum.map(args, &move_to_relationship_path(&1, relationship_path))}
|
||||
%Call{name: :exists, relationship_path: call_path} = call ->
|
||||
%{call | relationship_path: relationship_path ++ call_path}
|
||||
|
||||
%__MODULE__{expression: expression} = filter ->
|
||||
%{filter | expression: move_to_relationship_path(expression, relationship_path)}
|
||||
|
|
|
@ -281,14 +281,16 @@ defmodule Ash.Filter.Runtime do
|
|||
%resource{} ->
|
||||
Ash.Filter.hydrate_refs(expression, %{
|
||||
resource: resource,
|
||||
public?: false
|
||||
public?: false,
|
||||
parent_stack: parent_stack(parent)
|
||||
})
|
||||
|
||||
_ ->
|
||||
if resource do
|
||||
Ash.Filter.hydrate_refs(expression, %{
|
||||
resource: resource,
|
||||
public?: false
|
||||
public?: false,
|
||||
parent_stack: parent_stack(parent)
|
||||
})
|
||||
else
|
||||
{:ok, expression}
|
||||
|
@ -657,14 +659,16 @@ defmodule Ash.Filter.Runtime do
|
|||
%resource{} ->
|
||||
Ash.Filter.hydrate_refs(expression, %{
|
||||
resource: resource,
|
||||
public?: false
|
||||
public?: false,
|
||||
parent_stack: parent_stack(parent)
|
||||
})
|
||||
|
||||
_ ->
|
||||
if resource do
|
||||
Ash.Filter.hydrate_refs(expression, %{
|
||||
resource: resource,
|
||||
public?: false
|
||||
public?: false,
|
||||
parent_stack: parent_stack(parent)
|
||||
})
|
||||
else
|
||||
{:ok, expression}
|
||||
|
@ -941,4 +945,7 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp parent_stack(nil), do: []
|
||||
defp parent_stack(%resource{}), do: [resource]
|
||||
end
|
||||
|
|
|
@ -47,6 +47,9 @@ defmodule Ash.Filter.TemplateHelpers do
|
|||
@doc "A helper for creating a reference"
|
||||
def ref(name), do: {:_ref, [], name}
|
||||
|
||||
@doc "A helper for creating a parent reference"
|
||||
def parent(expr), do: {:_parent, [], expr}
|
||||
|
||||
@doc "A helper for creating a reference to a related path"
|
||||
def ref(path, name), do: {:_ref, path, name}
|
||||
|
||||
|
|
|
@ -87,7 +87,8 @@ defmodule Ash.Helpers do
|
|||
end
|
||||
|
||||
@doc false
|
||||
def deep_merge_maps(left, right) when is_map(left) and is_map(right) do
|
||||
def deep_merge_maps(left, right)
|
||||
when is_map(left) and is_map(right) and not is_struct(left) and not is_struct(right) do
|
||||
Map.merge(left, right, fn _, left, right ->
|
||||
deep_merge_maps(left, right)
|
||||
end)
|
||||
|
|
|
@ -2137,14 +2137,22 @@ defmodule Ash.Query do
|
|||
end
|
||||
|
||||
@doc false
|
||||
def do_filter(query, %Ash.Filter{} = filter) do
|
||||
def do_filter(query, filter, opts \\ [])
|
||||
|
||||
def do_filter(query, %Ash.Filter{} = filter, opts) do
|
||||
query = to_query(query)
|
||||
|
||||
if Ash.DataLayer.data_layer_can?(query.resource, :filter) do
|
||||
new_filter =
|
||||
case query.filter do
|
||||
nil ->
|
||||
Ash.Filter.parse(query.resource, filter, query.aggregates, query.calculations)
|
||||
Ash.Filter.parse(
|
||||
query.resource,
|
||||
filter,
|
||||
query.aggregates,
|
||||
query.calculations,
|
||||
with_parent_stack(%{}, opts)
|
||||
)
|
||||
|
||||
existing_filter ->
|
||||
Ash.Filter.add_to_filter(
|
||||
|
@ -2152,16 +2160,21 @@ defmodule Ash.Query do
|
|||
filter,
|
||||
:and,
|
||||
query.aggregates,
|
||||
query.calculations
|
||||
query.calculations,
|
||||
with_parent_stack(%{}, opts)
|
||||
)
|
||||
end
|
||||
|
||||
case new_filter do
|
||||
{:ok, filter} ->
|
||||
case Ash.Filter.hydrate_refs(filter, %{
|
||||
resource: query.resource,
|
||||
public?: false
|
||||
}) do
|
||||
case Ash.Filter.hydrate_refs(
|
||||
filter,
|
||||
%{
|
||||
resource: query.resource,
|
||||
public?: false
|
||||
}
|
||||
|> with_parent_stack(opts)
|
||||
) do
|
||||
{:ok, result} ->
|
||||
%{query | filter: result}
|
||||
|
||||
|
@ -2177,10 +2190,10 @@ defmodule Ash.Query do
|
|||
end
|
||||
end
|
||||
|
||||
def do_filter(query, nil), do: to_query(query)
|
||||
def do_filter(query, []), do: to_query(query)
|
||||
def do_filter(query, nil, _opts), do: to_query(query)
|
||||
def do_filter(query, [], _opts), do: to_query(query)
|
||||
|
||||
def do_filter(query, statement) do
|
||||
def do_filter(query, statement, opts) do
|
||||
query = to_query(query)
|
||||
|
||||
if Ash.DataLayer.data_layer_can?(query.resource, :filter) do
|
||||
|
@ -2191,23 +2204,29 @@ defmodule Ash.Query do
|
|||
statement,
|
||||
:and,
|
||||
query.aggregates,
|
||||
query.calculations
|
||||
query.calculations,
|
||||
with_parent_stack(%{}, opts)
|
||||
)
|
||||
else
|
||||
Ash.Filter.parse(
|
||||
query.resource,
|
||||
statement,
|
||||
query.aggregates,
|
||||
query.calculations
|
||||
query.calculations,
|
||||
with_parent_stack(%{}, opts)
|
||||
)
|
||||
end
|
||||
|
||||
case filter do
|
||||
{:ok, filter} ->
|
||||
case Ash.Filter.hydrate_refs(filter, %{
|
||||
resource: query.resource,
|
||||
public?: false
|
||||
}) do
|
||||
case Ash.Filter.hydrate_refs(
|
||||
filter,
|
||||
%{
|
||||
resource: query.resource,
|
||||
public?: false
|
||||
}
|
||||
|> with_parent_stack(opts)
|
||||
) do
|
||||
{:ok, result} ->
|
||||
%{query | filter: result}
|
||||
|
||||
|
@ -2223,6 +2242,16 @@ defmodule Ash.Query do
|
|||
end
|
||||
end
|
||||
|
||||
defp with_parent_stack(context, opts) do
|
||||
if opts[:parent_stack] do
|
||||
parent_stack = List.wrap(opts[:parent_stack])
|
||||
|
||||
Map.update(context, :parent_stack, parent_stack, &(parent_stack ++ &1))
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lock the query results.
|
||||
|
||||
|
|
|
@ -417,6 +417,7 @@ defmodule Ash.Resource do
|
|||
:__meta__,
|
||||
:__metadata__,
|
||||
:__order__,
|
||||
:__lateral_join_source__,
|
||||
:*,
|
||||
:calculations,
|
||||
:aggregates,
|
||||
|
|
|
@ -98,6 +98,12 @@ defmodule Ash.Schema do
|
|||
)
|
||||
end
|
||||
|
||||
Module.put_attribute(
|
||||
__MODULE__,
|
||||
:ash_struct_fields,
|
||||
{:__lateral_join_source__, nil}
|
||||
)
|
||||
|
||||
struct_fields = Module.get_attribute(__MODULE__, :ash_struct_fields)
|
||||
Module.delete_attribute(__MODULE__, struct_fields_name)
|
||||
Module.register_attribute(__MODULE__, struct_fields_name, accumulate: true)
|
||||
|
@ -242,6 +248,12 @@ defmodule Ash.Schema do
|
|||
)
|
||||
end
|
||||
|
||||
Module.put_attribute(
|
||||
__MODULE__,
|
||||
:ash_struct_fields,
|
||||
{:__lateral_join_source__, nil}
|
||||
)
|
||||
|
||||
struct_fields = Module.get_attribute(__MODULE__, :ash_struct_fields)
|
||||
|
||||
Module.delete_attribute(__MODULE__, struct_fields_name)
|
||||
|
|
|
@ -189,6 +189,11 @@ defmodule Ash.Test.Actions.LoadTest do
|
|||
api(Ash.Test.Actions.LoadTest.Api2)
|
||||
end
|
||||
|
||||
has_many :posts_with_same_title, __MODULE__ do
|
||||
no_attributes? true
|
||||
filter expr(parent(title) == title and parent(id) != id)
|
||||
end
|
||||
|
||||
many_to_many(:categories, Ash.Test.Actions.LoadTest.Category,
|
||||
through: Ash.Test.Actions.LoadTest.PostCategory,
|
||||
destination_attribute_on_join_resource: :category_id,
|
||||
|
@ -344,6 +349,29 @@ defmodule Ash.Test.Actions.LoadTest do
|
|||
|> Map.get(:posts_in_same_category)
|
||||
end
|
||||
|
||||
test "parent expressions can be used for complex constraints" do
|
||||
post1 =
|
||||
Post
|
||||
|> new(%{title: "post1", category: "foo"})
|
||||
|> Api.create!()
|
||||
|
||||
post1_same =
|
||||
Post
|
||||
|> new(%{title: "post1", category: "bar"})
|
||||
|> Api.create!()
|
||||
|
||||
Post
|
||||
|> new(%{title: "post2", category: "baz"})
|
||||
|> Api.create!()
|
||||
|
||||
post1_same_id = post1_same.id
|
||||
|
||||
assert [%{id: ^post1_same_id}] =
|
||||
post1
|
||||
|> Api.load!(:posts_with_same_title)
|
||||
|> Map.get(:posts_with_same_title)
|
||||
end
|
||||
|
||||
test "it allows loading through manual relationships" do
|
||||
post1 =
|
||||
Post
|
||||
|
|
|
@ -258,13 +258,21 @@ defmodule Ash.Test.NotifierTest do
|
|||
|> Ash.Changeset.new(%{name: "foo"})
|
||||
|> Api.create!()
|
||||
|
||||
Post
|
||||
|> Ash.Changeset.new(%{name: "foo"})
|
||||
|> Ash.Changeset.manage_relationship(:related_posts, [post], type: :append_and_remove)
|
||||
|> Api.create!()
|
||||
|> Ash.Changeset.new(%{})
|
||||
|> Ash.Changeset.manage_relationship(:related_posts, [], type: :append_and_remove)
|
||||
|> Api.update!()
|
||||
assert %{related_posts: [_]} =
|
||||
post =
|
||||
Post
|
||||
|> Ash.Changeset.new(%{name: "foo"})
|
||||
|> Ash.Changeset.manage_relationship(:related_posts, [post],
|
||||
type: :append_and_remove
|
||||
)
|
||||
|> Api.create!()
|
||||
|> Api.load!(:related_posts)
|
||||
|
||||
assert %{related_posts: []} =
|
||||
post
|
||||
|> Ash.Changeset.new(%{})
|
||||
|> Ash.Changeset.manage_relationship(:related_posts, [], type: :append_and_remove)
|
||||
|> Api.update!()
|
||||
|
||||
assert_receive {:notification, %{action: %{type: :destroy}, resource: PostLink}}
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue