Packages
ash
3.4.70
3.29.3
3.29.2
3.29.1
3.29.0
3.28.0
3.27.8
3.27.7
3.27.6
3.27.5
3.27.4
3.27.3
3.27.2
3.27.1
3.27.0
3.26.0
3.25.2
3.25.1
3.25.0
retired
3.24.7
3.24.6
3.24.5
3.24.4
3.24.3
3.24.2
3.24.1
3.24.0
3.23.1
3.23.0
3.22.2
3.22.1
3.22.0
3.21.3
3.21.2
3.21.1
3.21.0
3.20.0
3.19.3
3.19.2
3.19.1
3.19.0
3.18.0
3.17.1
3.17.0
3.16.0
3.15.0
3.14.1
3.14.0
retired
3.13.2
3.13.1
3.13.0
3.12.0
3.11.3
3.11.2
3.11.1
3.11.0
3.10.1
3.10.0
3.9.0
3.8.0
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
retired
3.6.3
retired
3.6.2
3.6.1
3.6.0
3.5.43
3.5.42
3.5.41
3.5.40
3.5.39
3.5.38
3.5.37
3.5.36
3.5.35
3.5.34
3.5.33
3.5.32
3.5.31
3.5.30
3.5.29
3.5.28
3.5.27
3.5.26
3.5.25
3.5.24
3.5.23
3.5.22
3.5.21
3.5.20
3.5.19
3.5.18
3.5.17
3.5.16
3.5.15
3.5.14
3.5.13
3.5.12
3.5.11
3.5.10
3.5.9
3.5.8
3.5.7
3.5.6
3.5.5
3.5.4
3.5.3
3.5.2
3.5.1
3.5.0
3.4.74
retired
3.4.73
3.4.72
3.4.71
3.4.70
3.4.69
3.4.68
3.4.67
3.4.66
3.4.65
3.4.64
3.4.63
3.4.62
3.4.61
3.4.60
3.4.59
3.4.58
3.4.57
3.4.56
3.4.55
3.4.54
3.4.53
3.4.52
3.4.51
3.4.50
3.4.49
3.4.48
3.4.47
3.4.46
3.4.45
3.4.44
3.4.43
3.4.42
3.4.41
3.4.40
3.4.39
3.4.38
3.4.37
3.4.36
3.4.35
3.4.34
3.4.33
3.4.32
3.4.31
3.4.30
3.4.29
3.4.28
3.4.27
3.4.26
3.4.25
3.4.24
3.4.23
3.4.22
3.4.21
3.4.20
3.4.19
3.4.18
3.4.17
3.4.16
3.4.15
3.4.14
3.4.13
3.4.12
3.4.11
3.4.10
3.4.9
3.4.8
3.4.7
3.4.6
3.4.5
3.4.4
3.4.3
3.4.2
3.4.1
3.4.0
3.3.3
3.3.2
3.3.1
3.3.0
3.2.6
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.1.8
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.16
3.0.15
3.0.14
3.0.13
3.0.12
3.0.11
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.46
3.0.0-rc.45
3.0.0-rc.44
3.0.0-rc.43
3.0.0-rc.42
3.0.0-rc.41
3.0.0-rc.40
3.0.0-rc.39
3.0.0-rc.38
3.0.0-rc.37
3.0.0-rc.36
3.0.0-rc.35
3.0.0-rc.34
3.0.0-rc.33
3.0.0-rc.32
3.0.0-rc.31
3.0.0-rc.29
3.0.0-rc.28
3.0.0-rc.27
3.0.0-rc.26
3.0.0-rc.25
3.0.0-rc.24
3.0.0-rc.23
3.0.0-rc.22
3.0.0-rc.21
3.0.0-rc.20
3.0.0-rc.19
3.0.0-rc.18
3.0.0-rc.17
3.0.0-rc.16
3.0.0-rc.15
3.0.0-rc.14
3.0.0-rc.13
3.0.0-rc.12
3.0.0-rc.11
3.0.0-rc.10
3.0.0-rc.9
3.0.0-rc.8
3.0.0-rc.7
3.0.0-rc.6
3.0.0-rc.5
3.0.0-rc.4
3.0.0-rc.3
3.0.0-rc.1
3.0.0-rc.0
2.21.15
2.21.14
2.21.13
2.21.12
2.21.11
2.21.10
2.21.9
2.21.8
2.21.7
2.21.6
2.21.5
2.21.4
2.21.3
2.21.2
2.21.1
2.21.0
2.20.3
2.20.2
2.20.1
2.20.0
2.19.14
2.19.13
2.19.12
2.19.11
2.19.10
2.19.9
2.19.8
2.19.7
2.19.6
2.19.5
2.19.4
2.19.3
retired
2.19.2
retired
2.19.1
retired
2.19.0
retired
2.18.2
2.18.1
2.18.0
2.17.24
2.17.23
2.17.22
2.17.21
2.17.20
2.17.19
2.17.18
2.17.17
2.17.16
2.17.15
2.17.14
2.17.13
2.17.12
2.17.11
2.17.10
2.17.9
2.17.8
2.17.7
2.17.6
2.17.5
2.17.4
2.17.3
2.17.2
2.17.1
2.17.0
2.16.1
2.16.0
2.15.20
2.15.19
2.15.18
2.15.17
2.15.16
2.15.15
2.15.14
2.15.13
2.15.12
2.15.11
2.15.10
2.15.9
2.15.8
2.15.7
2.15.6
2.15.5
2.15.4
2.15.2
2.15.1
2.15.0
2.14.21
2.14.20
2.14.19
2.14.18
2.14.17
2.14.16
2.14.15
2.14.14
2.14.13
2.14.12
2.14.11
2.14.10
2.14.9
2.14.8
2.14.7
2.14.6
2.14.5
2.14.4
2.14.3
2.14.2
2.14.1
2.14.0
2.13.4
retired
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.11
2.11.10
2.11.9
2.11.8
2.11.7
2.11.6
2.11.5
2.11.4
2.11.3
2.11.2
2.11.1
2.11.0
2.11.0-rc.3
2.11.0-rc.2
2.11.0-rc.1
2.11.0-rc.0
2.10.2
2.10.1
2.10.0
2.9.29
2.9.28
2.9.27
2.9.26
2.9.25
2.9.24
2.9.23
2.9.22
2.9.21
2.9.20
2.9.19
2.9.18
2.9.17
2.9.16
2.9.15
2.9.14
2.9.13
2.9.12
2.9.11
2.9.10
2.9.9
2.9.8
2.9.7
2.9.6
2.9.5
2.9.4
2.9.3
2.9.2
2.9.1
2.9.0
2.8.1
2.8.0
2.7.1
2.7.0
2.6.31
2.6.30
2.6.29
2.6.28
2.6.27
2.6.26
2.6.25
2.6.24
2.6.23
2.6.22
2.6.21
2.6.20
2.6.19
2.6.18
2.6.17
2.6.16
2.6.15
2.6.14
2.6.13
2.6.11
2.6.10
2.6.9
2.6.8
2.6.7
2.6.6
2.6.5
2.6.4
2.6.3
2.6.2
2.6.1
2.6.0
2.5.16
2.5.15
2.5.14
2.5.13
2.5.12
2.5.11
2.5.10
2.5.9
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.5.0-rc.6
2.5.0-rc.5
2.5.0-rc.4
2.5.0-rc.3
2.5.0-rc.2
2.5.0-rc.1
2.5.0-rc.0
2.4.30
2.4.29
2.4.28
2.4.27
2.4.26
2.4.25
2.4.24
2.4.23
2.4.22
2.4.21
2.4.20
2.4.19
2.4.18
2.4.17
2.4.16
2.4.15
2.4.14
2.4.13
2.4.12
2.4.11
2.4.10
2.4.9
2.4.8
2.4.7
2.4.6
2.4.5
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.0
2.0.0-rc.15
2.0.0-rc.14
2.0.0-rc.13
2.0.0-rc.12
2.0.0-rc.11
2.0.0-rc.10
2.0.0-rc.9
2.0.0-rc.8
2.0.0-rc.7
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
2.0.0-pre.8
2.0.0-pre.7
2.0.0-pre.6
2.0.0-pre.5
2.0.0-pre.4
2.0.0-pre.3
2.0.0-pre.2
2.0.0-pre.1
2.0.0-pre.0
1.53.3
1.53.2
1.53.0
1.52.0-rc.22
1.52.0-rc.21
1.52.0-rc.20
1.52.0-rc.19
1.52.0-rc.18
1.52.0-rc.17
1.52.0-rc.16
1.52.0-rc.15
1.52.0-rc.14
1.52.0-rc.13
1.52.0-rc.12
1.52.0-rc.11
1.52.0-rc.10
1.52.0-rc.9
1.52.0-rc.8
1.52.0-rc.7
1.52.0-rc.6
1.52.0-rc.5
1.52.0-rc.4
1.52.0-rc.3
1.52.0-rc.2
1.52.0-rc.1
1.52.0-rc.0
1.51.2
1.51.1
retired
1.51.0
1.50.21
1.50.20
1.50.19
1.50.18
1.50.17
1.50.16
1.50.15
1.50.14
1.50.13
1.50.12
1.50.11
1.50.10
1.50.9
1.50.8
1.50.7
1.50.6
1.50.5
1.50.4
1.50.3
1.50.2
1.50.1
1.50.0
1.49.0
1.48.0-rc.30
1.48.0-rc.29
1.48.0-rc.28
1.48.0-rc.27
1.48.0-rc.26
1.48.0-rc.25
1.48.0-rc.24
1.48.0-rc.23
1.48.0-rc.22
1.48.0-rc.21
1.48.0-rc.20
1.48.0-rc.19
1.48.0-rc.18
1.48.0-rc.17
1.48.0-rc.16
1.48.0-rc.15
1.48.0-rc.14
1.48.0-rc.13
1.48.0-rc.12
1.48.0-rc.11
1.48.0-rc.10
1.48.0-rc.9
1.48.0-rc.8
1.48.0-rc.7
1.48.0-rc.6
1.48.0-rc.5
1.48.0-rc.4
1.48.0-rc.3
1.48.0-rc.2
1.48.0-rc.1
1.48.0-rc.0
1.47.12
1.47.11
1.47.10
1.47.9
1.47.8
1.47.7
1.47.6
1.47.5
1.47.4
1.47.3
1.47.2
1.47.1
1.47.0
1.46.13
1.46.12
1.46.11
1.46.10
1.46.9
1.46.8
1.46.7
1.46.6
1.46.5
1.46.4
1.46.3
1.46.2
1.46.1
1.46.0
1.45.0-rc9
1.45.0-rc8
1.45.0-rc7
1.45.0-rc6
1.45.0-rc5
1.45.0-rc4
1.45.0-rc3
1.45.0-rc20
1.45.0-rc2
1.45.0-rc19
1.45.0-rc18
1.45.0-rc17
1.45.0-rc16
1.45.0-rc15
1.45.0-rc14
1.45.0-rc13
1.45.0-rc12
1.45.0-rc11
1.45.0-rc10
1.45.0-rc1
1.45.0-rc0
1.44.13
1.44.12
1.44.11
1.44.10
1.44.9
1.44.8
1.44.7
1.44.6
1.44.5
1.44.4
1.44.3
1.44.2
1.44.1
1.44.0
1.43.12
1.43.11
1.43.10
1.43.9
1.43.8
1.43.7
1.43.6
1.43.5
1.43.4
1.43.3
1.43.2
1.43.1
1.43.0
1.42.0
1.41.12
1.41.11
1.41.10
1.41.9
1.41.8
1.41.7
1.41.6
1.41.5
1.41.4
1.41.3
1.41.2
1.41.1
1.41.0
1.40.0
1.39.7
1.39.6
1.39.5
1.39.4
1.39.3
1.39.2
1.39.1
1.39.0
1.38.0
1.37.2
1.37.1
1.37.0
1.36.22
1.36.21
1.36.19
1.36.18
1.36.17
1.36.16
1.36.15
1.36.14
1.36.13
1.36.12
1.36.11
1.36.10
1.36.9
1.36.8
1.36.7
1.36.6
1.36.5
1.36.4
1.36.3
1.36.2
1.36.0
1.35.1
1.35.0
1.34.9
1.34.8
1.34.7
1.34.6
1.34.5
1.34.4
1.34.3
1.34.2
1.34.1
1.34.0
1.33.0
1.32.2
1.32.1
1.32.0
1.31.1
1.31.0
1.30.2
1.30.1
1.29.0-rc1
1.29.0-rc0
1.28.1
1.28.0
1.27.1
1.27.0
1.26.13
1.26.12
1.26.11
1.26.10
1.26.9
1.26.8
1.26.7
1.26.6
1.26.5
1.26.4
1.26.2
1.26.1
1.26.0
1.25.8
1.25.7
1.25.6
1.25.5
1.25.4
1.25.3
1.25.2
1.25.1
1.25.0
1.24.2
1.24.1
1.24.0
1.23.3
1.23.2
1.23.1
1.23.0
1.22.1
1.22.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.1
1.18.0
1.17.1
1.17.0
1.16.2
1.15.1
1.15.0
1.14.0
1.13.4
1.13.3
1.13.2
1.13.1
1.13.0
1.12.0
1.11.1
1.11.0
1.10.0
1.9.0
1.8.0
1.7.0
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.1
1.4.0
1.3.1
1.3.0
1.2.1
1.2.0
1.1.3
1.1.2
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
0.13.1
0.13.0
0.12.0
0.10.0
0.9.1
0.9.0
0.8.0
0.7.0
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.9
0.1.8
0.1.3
0.1.1
0.1.0
A declarative, extensible framework for building Elixir applications.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/ash/actions/read/relationships.ex
defmodule Ash.Actions.Read.Relationships do
@moduledoc false
require Ash.Query
import Ash.Expr
def load([], _query, _lazy?, _reuse_values?) do
{:ok, []}
end
def load(record, query, lazy?, reuse_values?) when not is_list(record) do
case load([record], query, lazy?, reuse_values?) do
{:ok, [record]} -> {:ok, record}
{:error, error} -> {:error, error}
end
end
def load(records, %{load: load}, _lazy?, _reuse_values?) when load in [%{}, [], nil] do
{:ok, records}
end
def load(records, query, lazy?, reuse_values?) do
query.load
|> with_related_queries(query, records, lazy?)
|> fetch_related_records(query, records, reuse_values?)
|> attach_related_records(records)
end
defp attach_related_records(relationships_queries_and_related_records, records) do
Enum.reduce_while(relationships_queries_and_related_records, {:ok, records}, fn
{relationship, related_query, {:ok, related_records}}, {:ok, records} ->
{:cont,
{:ok, do_attach_related_records(records, relationship, related_records, related_query)}}
{relationship, _related_query, {:error, %Ash.Error.Forbidden{} = error}}, _ ->
if relationship.allow_forbidden_field? do
{:cont,
{:ok,
Enum.map(records, fn record ->
Map.put(record, relationship.name, %Ash.ForbiddenField{
type: :relationship,
field: relationship.name
})
end)}}
else
{:halt, {:error, Ash.Error.set_path(error, relationship.name)}}
end
{relationship, _related_query, {:error, error}}, _ ->
{:halt, {:error, Ash.Error.set_path(error, relationship.name)}}
{:__exception__, error, stacktrace}, _ ->
{:halt, {:error, Ash.Error.to_ash_error(error, stacktrace)}}
end)
end
defp fetch_related_records(batch, query, records, reuse_values?, acc \\ [])
defp fetch_related_records([], _query, _records, _reuse_values?, acc) do
Enum.map(acc, fn
{a, b, %Task{} = task} ->
{a, b, Task.await(task, :infinity)}
%Task{} = task ->
Task.await(task, :infinity)
other ->
other
end)
end
defp fetch_related_records([first | rest], query, records, reuse_values?, acc) do
result =
case first do
{relationship, {:lazy, query}} ->
{relationship, {:lazy, query},
lazy_related_records(records, relationship, query, Enum.empty?(rest), reuse_values?)}
{relationship, %{valid?: true} = related_query} ->
do_fetch_related_records(query, records, relationship, related_query, Enum.empty?(rest))
{relationship, %{errors: errors} = related_query} ->
{relationship, related_query, {:error, errors}}
end
fetch_related_records(rest, query, records, reuse_values?, [result | acc])
end
defp lazy_related_records(records, relationship, related_query, last?, reuse_values?) do
primary_key = Ash.Resource.Info.primary_key(relationship.source)
related_records_with_lazy_join_source =
Enum.flat_map(records, fn record ->
record_pkey = Map.take(record, primary_key)
record
|> Map.get(relationship.name)
|> case do
%Ash.NotLoaded{} ->
[]
%Ash.ForbiddenField{} ->
[]
record_or_records ->
record_or_records
end
|> List.wrap()
|> Enum.map(&Ash.Resource.set_metadata(&1, %{lazy_join_source: record_pkey}))
end)
Ash.Actions.Read.AsyncLimiter.async_or_inline(
related_query,
Ash.Context.to_opts(related_query.context),
last?,
fn ->
Ash.load(related_records_with_lazy_join_source, related_query,
lazy?: true,
reuse_values?:
reuse_values? || related_query.context[:private][:reuse_values?] || false,
domain: related_query.domain,
actor: related_query.context.private[:actor],
tenant: related_query.tenant,
authorize?: related_query.context.private[:authorize?]
)
end
)
end
defp with_related_queries(load, query, records, lazy?) do
Enum.map(load, fn {relationship_name, related_query} ->
lazy? = lazy? || related_query.context[:private][:lazy?]
if lazy? && Ash.Resource.loaded?(records, relationship_name, lists: :any) do
relationship = Ash.Resource.Info.relationship(query.resource, relationship_name)
related_query =
case related_query do
[] -> Ash.Query.new(relationship.destination)
query -> query
end
domain = Ash.Domain.Info.related_domain(related_query, relationship, query.domain)
{read_action_name, arguments} =
case related_query do
%Ash.Query{action: %{name: name}, arguments: arguments} ->
{name, arguments}
_ ->
read_action_name =
relationship.read_action ||
Ash.Resource.Info.primary_action!(relationship.destination, :read).name
{read_action_name, %{}}
end
{relationship,
{:lazy,
related_query
|> Map.put(
:domain,
Ash.Domain.Info.related_domain(related_query, relationship, query.domain)
)
|> Ash.Query.set_tenant(query.tenant)
|> Ash.Query.set_context(%{
private: %{async_limiter: query.context[:private][:async_limiter]}
})
|> Ash.Query.for_read(
read_action_name,
arguments,
domain: domain,
authorize?: query.context[:private][:authorize?],
actor: query.context[:private][:actor],
tracer: query.context[:private][:tracer]
)}}
else
related_query(relationship_name, records, related_query, query)
end
end)
end
@doc false
def related_query(relationship_name, records, related_query, query) do
relationship = Ash.Resource.Info.relationship(query.resource, relationship_name)
{read_action_name, arguments} =
case related_query do
%Ash.Query{action: %{name: name}, arguments: arguments} ->
{name, arguments}
_ ->
read_action_name =
relationship.read_action ||
Ash.Resource.Info.primary_action!(relationship.destination, :read).name
{read_action_name, %{}}
end
domain = Ash.Domain.Info.related_domain(related_query, relationship, query.domain)
parent_stack =
[
query.resource | Ash.Actions.Read.parent_stack_from_context(query.context)
]
related_query =
related_query
|> Ash.Query.set_context(%{
private: %{async_limiter: query.context[:private][:async_limiter]}
})
|> Ash.Query.set_tenant(query.tenant)
|> Ash.Query.for_read(
read_action_name,
arguments,
domain: domain,
authorize?: query.context[:private][:authorize?],
actor: query.context[:private][:actor],
tracer: query.context[:private][:tracer]
)
|> Ash.Query.sort(relationship.sort)
|> Ash.Query.do_filter(relationship.filter, parent_stack: parent_stack)
|> Ash.Query.set_context(relationship.context)
|> Ash.Query.set_context(%{private: %{loading_relationship?: true}})
|> hydrate_refs(query.context[:private][:actor], relationship.source)
|> with_lateral_join_query(query, relationship, records)
if !related_query.context[:data_layer][:lateral_join_source] &&
related_query.distinct not in [[], nil] do
raise ArgumentError, message: "Cannot yet use `distinct` when loading related records"
end
{relationship, related_query}
end
defp with_lateral_join_query(related_query, source_query, relationship, records) do
if lateral_join?(related_query, source_query, relationship, records) do
lateral_join_source_path =
if relationship.type == :many_to_many do
join_relationship =
Ash.Resource.Info.relationship(source_query.resource, relationship.join_relationship)
through_query =
relationship.through
|> Ash.Query.for_read(
join_relationship.read_action ||
Ash.Resource.Info.primary_action!(relationship.through, :read).name,
%{},
authorize?: source_query.context[:private][:authorize?],
actor: source_query.context[:private][:actor],
tenant: source_query.tenant,
tracer: source_query.context[:private][:tracer],
domain: join_relationship.domain || related_query.domain
)
|> Ash.Query.do_filter(join_relationship.filter)
|> Ash.Query.set_context(%{
accessing_from: %{source: relationship.source, name: relationship.join_relationship}
})
|> Ash.Query.select([
relationship.source_attribute_on_join_resource,
relationship.destination_attribute_on_join_resource
])
|> hydrate_refs(source_query.context[:private][:actor], relationship.source)
if source_query.context[:private][:authorize?] do
case Ash.can(
through_query,
source_query.context[:private][:actor],
return_forbidden_error?: true,
pre_flight?: false,
alter_source?: true,
run_queries?: false,
base_query: through_query
) do
{:ok, true} ->
{:ok,
[
{clear_lateral_join_source(source_query), relationship.source_attribute,
relationship.source_attribute_on_join_resource, relationship},
{clear_lateral_join_source(through_query),
relationship.destination_attribute_on_join_resource,
relationship.destination_attribute, join_relationship}
]}
{:ok, true, authorized_through_query} ->
{:ok,
[
{clear_lateral_join_source(source_query), relationship.source_attribute,
relationship.source_attribute_on_join_resource, relationship},
{clear_lateral_join_source(authorized_through_query),
relationship.destination_attribute_on_join_resource,
relationship.destination_attribute, join_relationship}
]}
{:ok, false, error} ->
{:error, Ash.Error.set_path(error, join_relationship.name)}
{:error, error} ->
{:error, Ash.Error.set_path(error, join_relationship.name)}
end
else
{:ok,
[
{clear_lateral_join_source(source_query), relationship.source_attribute,
relationship.source_attribute_on_join_resource, relationship},
{clear_lateral_join_source(through_query),
relationship.destination_attribute_on_join_resource,
relationship.destination_attribute, join_relationship}
]}
end
else
{:ok,
[
{clear_lateral_join_source(source_query), relationship.source_attribute,
relationship.destination_attribute, relationship}
]}
end
case lateral_join_source_path do
{:ok, lateral_join_source_path} ->
Ash.Query.set_context(related_query, %{
data_layer: %{
lateral_join_source: {records, lateral_join_source_path}
}
})
{:error, error} ->
Ash.Query.add_error(related_query, error)
end
else
related_query
end
end
defp hydrate_refs(query, actor, parent) do
query.filter
|> Ash.Expr.fill_template(actor, %{}, query.context)
|> Ash.Filter.hydrate_refs(%{
resource: query.resource,
parent_stack: [parent],
public?: false
})
|> case do
{:ok, hydrated} ->
%{query | filter: hydrated}
{:error, error} ->
Ash.Query.add_error(query, error)
end
end
defp clear_lateral_join_source(source_query) do
Ash.Query.unset(source_query, [:load, :select, :sort])
end
defp do_fetch_related_records(
query,
records,
%{manual: {module, opts}} = relationship,
related_query,
last?
) do
parent_stack = [
query.resource | Ash.Actions.Read.parent_stack_from_context(query.context)
]
Ash.Actions.Read.AsyncLimiter.async_or_inline(
related_query,
Ash.Context.to_opts(related_query.context),
last?,
fn ->
result =
module.load(records, opts, %Ash.Resource.ManualRelationship.Context{
relationship: relationship,
query:
related_query
|> Ash.Query.sort(relationship.sort)
|> Ash.Query.do_filter(relationship.filter)
|> Map.put(:load, [])
|> Ash.Query.set_context(%{
accessing_from: %{source: relationship.source, name: relationship.name},
parent_stack: parent_stack
}),
actor: related_query.context[:private][:actor],
authorize?: related_query.context[:private][:authorize?],
domain: related_query.domain,
tenant: related_query.tenant
})
|> case do
{:ok, records} ->
records
|> Enum.flat_map(fn {key, value} ->
value
|> List.wrap()
|> Enum.map(&Ash.Resource.put_metadata(&1, :manual_key, key))
end)
|> Ash.load(related_query,
domain: related_query.domain,
actor: related_query.context[:private][:actor],
authorize?: related_query.context[:private][:authorize?],
tenant: related_query.tenant
)
|> case do
{:ok, results} ->
{:ok, regroup_manual_results(results, relationship)}
{:error, error} ->
{:error, error}
end
{:error, error} ->
{:error, error}
end
{relationship, related_query, result}
end
)
end
defp do_fetch_related_records(
query,
_records,
%{no_attributes?: true} = relationship,
related_query,
last?
) do
parent_stack = [
query.resource | Ash.Actions.Read.parent_stack_from_context(query.context)
]
Ash.Actions.Read.AsyncLimiter.async_or_inline(
related_query,
Ash.Context.to_opts(related_query.context),
last?,
fn ->
result =
related_query
|> select_destination_attribute(relationship)
|> Ash.Query.set_context(%{
accessing_from: %{source: relationship.source, name: relationship.name},
parent_stack: parent_stack
})
|> Ash.Actions.Read.unpaginated_read(nil,
authorize_with: relationship.authorize_read_with
)
{relationship, related_query, result}
end
)
end
defp do_fetch_related_records(
query,
_records,
relationship,
%{context: %{data_layer: %{lateral_join_source: {_, _}}}} = related_query,
last?
) do
parent_stack = [
query.resource | Ash.Actions.Read.parent_stack_from_context(query.context)
]
Ash.Actions.Read.AsyncLimiter.async_or_inline(
related_query,
Ash.Context.to_opts(related_query.context),
last?,
fn ->
result =
related_query
|> select_destination_attribute(relationship)
|> Ash.Query.set_context(%{
accessing_from: %{source: relationship.source, name: relationship.name},
parent_stack: parent_stack
})
|> Ash.Actions.Read.read_and_return_unpaged(nil,
authorize_with: relationship.authorize_read_with
)
{relationship, related_query, result}
end
)
end
defp do_fetch_related_records(
query,
records,
%{type: :many_to_many} = relationship,
related_query,
last?
) do
record_ids =
Enum.map(records, fn record ->
Map.get(record, relationship.source_attribute)
end)
join_relationship =
Ash.Resource.Info.relationship(relationship.source, relationship.join_relationship)
join_query =
relationship.through
|> Ash.Query.filter(^ref(relationship.source_attribute_on_join_resource) in ^record_ids)
|> Ash.Query.do_filter(join_relationship.filter)
|> Ash.Query.set_context(%{
accessing_from: %{source: relationship.source, name: relationship.join_relationship},
parent_stack: [query.resource | Ash.Actions.Read.parent_stack_from_context(query.context)]
})
|> Ash.Query.select(
[
relationship.source_attribute_on_join_resource,
relationship.destination_attribute_on_join_resource
] ++ Ash.Resource.Info.primary_key(join_relationship.destination)
)
|> Ash.Query.for_read(
join_relationship.read_action ||
Ash.Resource.Info.primary_action!(relationship.through, :read).name,
%{},
authorize?: related_query.context[:private][:authorize?],
actor: related_query.context[:private][:actor],
tracer: related_query.context[:private][:tracer],
tenant: related_query.tenant,
domain:
Ash.Domain.Info.related_domain(
related_query,
join_relationship,
related_query.domain
)
)
parent_stack = [
query.resource | Ash.Actions.Read.parent_stack_from_context(query.context)
]
Ash.Actions.Read.AsyncLimiter.async_or_inline(
related_query,
Ash.Context.to_opts(related_query.context),
last?,
fn ->
case Ash.Actions.Read.unpaginated_read(join_query, nil) do
{:ok, join_records} ->
{join_id_mapping, destination_ids} =
Enum.reduce(join_records, {%{}, MapSet.new()}, fn join_record,
{mapping, destination_ids} ->
destination_value =
Map.get(join_record, relationship.destination_attribute_on_join_resource)
source_value =
Map.get(join_record, relationship.source_attribute_on_join_resource)
new_destination_ids = MapSet.put(destination_ids, destination_value)
new_mapping =
Map.update(
mapping,
destination_value,
[
source_value
],
&[source_value | &1]
)
{new_mapping, new_destination_ids}
end)
related_query =
if related_query.page do
if Ash.Actions.Sort.sorting_on_identity?(related_query) do
related_query
else
Ash.Query.sort(
related_query,
Ash.Resource.Info.primary_key(related_query.resource)
)
end
else
related_query
end
related_query
|> select_destination_attribute(relationship)
|> Ash.Query.sort(relationship.sort)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.filter(^ref(relationship.destination_attribute) in ^destination_ids)
|> Ash.Query.set_context(%{
accessing_from: %{source: relationship.source, name: relationship.name},
parent_stack: parent_stack
})
|> Map.put(:page, nil)
|> Ash.Actions.Read.unpaginated_read(nil,
authorize_with: relationship.authorize_read_with
)
|> case do
{:ok, records} ->
{relationship, related_query,
{:ok,
Enum.flat_map(records, fn record ->
Enum.map(
join_id_mapping[Map.get(record, relationship.destination_attribute)] || [],
fn lateral_join_source ->
Map.put(
record,
:__lateral_join_source__,
lateral_join_source
)
end
)
end)}}
{:error, error} ->
{relationship, related_query, {:error, error}}
end
{:error, error} ->
{relationship, related_query, {:error, error}}
end
end
)
end
defp do_fetch_related_records(query, records, relationship, related_query, last?) do
destination_attributes = Enum.map(records, &Map.get(&1, relationship.source_attribute))
parent_stack = [
query.resource | Ash.Actions.Read.parent_stack_from_context(query.context)
]
Ash.Actions.Read.AsyncLimiter.async_or_inline(
related_query,
Ash.Context.to_opts(related_query.context),
last?,
fn ->
result =
related_query
|> select_destination_attribute(relationship)
|> Ash.Query.filter(^ref(relationship.destination_attribute) in ^destination_attributes)
|> Ash.Query.unset([:limit, :offset, :distinct, :distinct_sort, :page])
|> Ash.Query.set_context(%{
accessing_from: %{source: relationship.source, name: relationship.name},
parent_stack: parent_stack
})
|> Ash.Actions.Read.unpaginated_read(nil,
authorize_with: relationship.authorize_read_with
)
{relationship, related_query, result}
end
)
end
defp regroup_manual_results(records, %{cardinality: :many}) do
Enum.group_by(records, & &1.__metadata__.manual_key, &delete_manual_key/1)
end
defp regroup_manual_results(records, %{cardinality: :one}) do
Map.new(records, &{&1.__metadata__.manual_key, delete_manual_key(&1)})
end
defp delete_manual_key(record) do
Map.update!(record, :__metadata__, &Map.delete(&1, :manual_key))
end
defp select_destination_attribute(related_query, relationship) do
if Map.get(relationship, :no_attributes?) ||
(Map.get(relationship, :manual) &&
!Ash.Resource.Info.attribute(
relationship.destination,
relationship.destination_attribute
)) do
related_query
else
Ash.Query.ensure_selected(related_query, [relationship.destination_attribute])
end
end
defp do_attach_related_records(
[%resource{} | _] = records,
relationship,
related_records,
{:lazy, _related_query}
) do
pkey = Ash.Resource.Info.primary_key(resource)
Enum.map(records, fn record ->
record_pkey = Map.take(record, pkey)
related = Enum.filter(related_records, &(&1.__metadata__.lazy_join_source == record_pkey))
related =
case relationship.cardinality do
:many ->
related
:one ->
Enum.at(related, 0)
end
case Map.get(record, relationship.name) do
%Ash.ForbiddenField{} -> record
%Ash.NotLoaded{} -> record
_ -> Map.put(record, relationship.name, related)
end
end)
end
defp do_attach_related_records(
[_ | _] = records,
relationship,
%Ash.Page.Unpaged{} = unpaged,
%{context: %{data_layer: %{lateral_join_source: {_records, lateral_join_source_path}}}} =
related_query
) do
%Ash.Page.Unpaged{
related_records: related_records,
opts: opts
} = unpaged
attach_fun =
if relationship.cardinality == :many do
fn record, relationship_name, value ->
count_key =
Ash.Actions.Read.paginated_relationship_count_aggregate_name(relationship.name)
# Retrieve the count (if present) while deleting it from the record aggregates
{count, record} = pop_in(record.aggregates[count_key])
# We scope the lateral join to the specific record, so that next runs of rerun
# just fetch the entries related to this record
related_query =
Ash.Query.set_context(related_query, %{
data_layer: %{lateral_join_source: {[record], lateral_join_source_path}}
})
page =
Ash.Actions.Read.to_page(
value,
related_query.action,
count,
related_query.sort,
related_query,
related_query,
opts
)
attach_related(record, relationship_name, page)
end
else
&attach_related/3
end
attach_lateral_join_related_records(
records,
relationship,
related_records,
attach_fun
)
end
defp do_attach_related_records(
[_ | _] = records,
relationship,
related_records,
%{context: %{data_layer: %{lateral_join_source: {_, _}}}}
) do
attach_lateral_join_related_records(records, relationship, related_records)
end
defp do_attach_related_records(
records,
%{type: :many_to_many} = relationship,
related_records,
related_query
) do
do_attach_related_records(
records,
relationship,
related_records,
Ash.Query.set_context(related_query, %{data_layer: %{lateral_join_source: {nil, nil}}})
)
|> Enum.map(fn row ->
Map.update!(row, relationship.name, fn related_records ->
apply_runtime_query_operations(
row,
relationship,
related_records,
related_query
)
end)
end)
end
defp do_attach_related_records(
[%resource{} | _] = records,
%{manual: {_module, _opts}} = relationship,
map,
_related_query
) do
default =
case relationship.cardinality do
:one ->
nil
:many ->
[]
end
if Ash.Resource.Info.primary_key_simple_equality?(resource) do
pkey = Ash.Resource.Info.primary_key(resource)
single_match? =
case pkey do
[_] -> true
_ -> false
end
Enum.map(records, fn record ->
value =
if single_match? do
case Map.fetch(map, Map.get(record, Enum.at(pkey, 0))) do
{:ok, value} -> {:ok, value}
:error -> Map.fetch(map, Map.take(record, pkey))
end
else
Map.fetch(map, Map.take(record, pkey))
end
case value do
{:ok, result} ->
Map.put(record, relationship.name, result)
:error ->
Map.put(record, relationship.name, default)
end
end)
else
pkey = Ash.Resource.Info.primary_key(resource)
case pkey do
[pkey_key] ->
Enum.map(records, fn record ->
pkey_values = Map.take(record, pkey)
value =
Enum.find_value(map, fn {key, value} ->
if is_map(key) do
if resource.primary_key_matches?(key, pkey_values) do
{:ok, value}
end
else
if resource.primary_key_matches?(%{pkey_key => key}, pkey_values) do
{:ok, value}
end
end
end) || :error
case value do
{:ok, result} ->
Map.put(record, relationship.name, result)
:error ->
Map.put(record, relationship.name, default)
end
end)
_pkeys ->
Enum.map(records, fn record ->
pkey_values = Map.take(record, pkey)
value =
Enum.find_value(map, fn {key, value} ->
if resource.primary_key_matches?(key, pkey_values) do
{:ok, value}
end
end) || :error
case value do
{:ok, result} ->
Map.put(record, relationship.name, result)
:error ->
Map.put(record, relationship.name, default)
end
end)
end
end
end
defp do_attach_related_records(
records,
%{no_attributes?: true} = relationship,
related_records,
_related_query
) do
Enum.map(records, fn record ->
Map.put(record, relationship.name, related_records)
end)
end
defp do_attach_related_records(records, relationship, related_records, related_query) do
attribute = Ash.Resource.Info.attribute(relationship.source, relationship.source_attribute)
simple_equality? = Ash.Type.simple_equality?(attribute.type)
related =
if simple_equality? do
if relationship.cardinality == :one do
Map.new(
Enum.reverse(related_records),
&{Map.get(&1, relationship.destination_attribute), &1}
)
else
Enum.group_by(related_records, &Map.get(&1, relationship.destination_attribute))
end
else
related_records
end
default =
if relationship.cardinality == :one do
nil
else
[]
end
if simple_equality? do
Enum.map(records, fn record ->
value = Map.get(record, relationship.source_attribute)
if relationship.cardinality == :many do
Map.put(
record,
relationship.name,
apply_runtime_query_operations(
record,
relationship,
Map.get(related, value) || default,
related_query
)
)
else
Map.put(
record,
relationship.name,
apply_runtime_query_operations(
record,
relationship,
Enum.at(List.wrap(Map.get(related, value) || default), 0),
related_query
)
)
end
end)
else
Enum.map(records, fn record ->
value = Map.get(record, relationship.source_attribute)
if relationship.cardinality == :many do
related
|> Enum.filter(fn result ->
destination_value = Map.get(result, relationship.destination_attribute)
Ash.Type.equal?(attribute.type, value, destination_value)
end)
|> then(fn result ->
result =
apply_runtime_query_operations(record, relationship, {:ok, result}, related_query)
put_result(record, result, relationship.name, default)
end)
else
related
|> Enum.find_value(:error, fn result ->
destination_value = Map.get(result, relationship.destination_attribute)
if Ash.Type.equal?(attribute.type, value, destination_value) do
{:ok, result}
end
end)
|> then(fn result ->
result =
apply_runtime_query_operations(record, relationship, result, related_query)
put_result(record, result, relationship.name, default)
end)
end
end)
end
end
defp apply_runtime_query_operations(record, relationship, {:ok, value}, related_query) do
{:ok, apply_runtime_query_operations(record, relationship, value, related_query)}
end
defp apply_runtime_query_operations(_record, _relationship, :error, _related_query), do: :error
defp apply_runtime_query_operations(_record, _relationship, empty, _related_query)
when empty in [nil, []],
do: empty
defp apply_runtime_query_operations(record, relationship, value, related_query)
when not is_list(value) do
record |> apply_runtime_query_operations(relationship, [value], related_query) |> Enum.at(0)
end
defp apply_runtime_query_operations(record, relationship, value, related_query) do
value
|> apply_runtime_offset(related_query)
|> apply_runtime_limit(related_query)
|> apply_runtime_pagination(record, relationship, related_query)
end
defp apply_runtime_pagination(
value,
source_record,
relationship,
%{page: page_opts} = related_query
)
when not is_nil(page_opts) do
pagination_type =
cond do
related_query.action.pagination.keyset? && (page_opts[:before] || page_opts[:after]) ->
:keyset
page_opts[:offset] ->
:offset
related_query.action.pagination.offset? && related_query.action.pagination.keyset? ->
:offset
related_query.action.pagination.offset? ->
:offset
true ->
:keyset
end
count = Map.get(source_record.aggregates, "__paginated_#{relationship.name}_count__")
limit =
page_opts[:limit] ||
related_query.action.pagination.default_limit ||
related_query.action.pagination.max_page_size || 250
value =
if pagination_type == :keyset && (page_opts[:before] || page_opts[:after]) do
after_or_before =
if page_opts[:before] do
:before
else
:after
end
# Apparently dialyzer hated me trying to write this as a *with* statement???
# giving up and making it a case statement because I don't have time for this shit
case Ash.Page.Keyset.filter(
related_query,
page_opts[:before] || page_opts[:after],
related_query.sort,
after_or_before
) do
{:ok, filter} ->
case Ash.Query.do_filter(related_query.resource, filter) do
%{valid?: true} = query ->
case Ash.Filter.Runtime.filter_matches(
related_query.domain,
value,
query.filter,
tenant: query.tenant,
actor: query.actor
) do
{:ok, value} ->
value
{:error, error} ->
raise Ash.Error.to_ash_error(error)
end
%{valid?: false, errors: errors} ->
raise Ash.Error.to_ash_error(errors)
end
{:error, error} ->
raise Ash.Error.to_ash_error(error)
end
else
value
end
{value, rest} =
value
|> Enum.drop(page_opts[:offset] || 0)
|> Enum.split(limit)
more? = !Enum.empty?(rest)
case pagination_type do
:offset ->
Ash.Page.Offset.new(value, count, related_query, more?, [])
:keyset ->
Ash.Page.Keyset.new(value, count, related_query.sort, related_query, more?, [])
end
end
defp apply_runtime_pagination(value, _, _, _) do
value
end
defp apply_runtime_offset(value, %{offset: nil}), do: value
defp apply_runtime_offset(value, %{offset: offset}), do: Enum.drop(value, offset)
defp apply_runtime_limit(value, %{limit: nil}), do: value
defp apply_runtime_limit(value, %{limit: limit}), do: Enum.take(value, limit)
defp put_result(record, {:ok, match}, key, _default) do
Map.put(
record,
key,
match
)
end
defp put_result(record, :error, key, default) do
Map.put(record, key, default)
end
defp attach_related(record, relationship_name, value) do
Map.put(record, relationship_name, value)
end
defp attach_lateral_join_related_records(
[%resource{} | _] = records,
relationship,
related_records,
attach_fun \\ &attach_related/3
) do
source_attribute =
if Map.get(relationship, :no_attributes?) do
nil
else
Ash.Resource.Info.attribute(relationship.source, relationship.source_attribute)
end
pkey_simple_equality? = Ash.Resource.Info.primary_key_simple_equality?(relationship.source)
source_attribute_simple_equality? =
is_nil(source_attribute) || Ash.Type.simple_equality?(source_attribute.type)
primary_key = Ash.Resource.Info.primary_key(resource)
if pkey_simple_equality? && source_attribute_simple_equality? do
values =
if relationship.cardinality == :many do
Enum.group_by(related_records, & &1.__lateral_join_source__)
else
Map.new(Enum.reverse(related_records), &{&1.__lateral_join_source__, &1})
end
default =
if relationship.cardinality == :many do
[]
else
nil
end
if source_attribute do
Enum.map(records, fn record ->
with :error <- Map.fetch(values, Map.take(record, primary_key)),
:error <- Map.fetch(values, Map.get(record, relationship.source_attribute)) do
attach_fun.(record, relationship.name, default)
else
{:ok, value} ->
attach_fun.(record, relationship.name, value)
end
end)
else
Enum.map(records, fn record ->
case Map.fetch(values, Map.take(record, primary_key)) do
{:ok, value} ->
attach_fun.(record, relationship.name, value)
:error ->
attach_fun.(record, relationship.name, default)
end
end)
end
else
Enum.map(records, fn record ->
func =
if relationship.cardinality == :one do
:find
else
:filter
end
related =
apply(Enum, func, [
related_records,
fn related_record ->
if is_map(related_record.__lateral_join_source__) do
resource.primary_key_matches?(record, related_record.__lateral_join_source__)
else
Ash.Type.equal?(
source_attribute.type,
related_record.__lateral_join_source__,
Map.get(record, relationship.source_attribute)
)
end
end
])
attach_fun.(record, relationship.name, related)
end)
end
end
defp lateral_join?(%{action: action} = query, source_query, relationship, source_data) do
if action.manual do
raise_if_parent_expr!(relationship, query, "manual actions")
false
else
{offset, limit} = offset_and_limit(query)
resources =
[relationship.source, Map.get(relationship, :through), relationship.destination]
|> Enum.reject(&is_nil/1)
has_distinct? = query.distinct not in [[], nil]
has_page? = query.page not in [nil, false]
cond do
!Ash.DataLayer.data_layer_can?(
relationship.source,
{:lateral_join, resources}
) ->
false
is_many_to_many_not_unique_on_join?(relationship, query, source_query) ->
raise_if_parent_expr!(
relationship,
query,
"many to many relationships that don't have unique constraints on their join resource attributes"
)
false
Map.get(relationship, :from_many?) ->
true
limit == 1 && is_nil(relationship.context) && is_nil(relationship.filter) &&
is_nil(relationship.sort) && relationship.cardinality != :many ->
has_parent_expr?(relationship, query.context, query.domain)
limit == 1 && (source_data == :unknown || Enum.count_until(source_data, 2) == 1) &&
relationship.type != :many_to_many ->
has_parent_expr?(relationship, query.context, query.domain)
has_parent_expr?(relationship, query.context, query.domain) ->
true
relationship.type == :many_to_many &&
Ash.DataLayer.prefer_lateral_join_for_many_to_many?(
Ash.DataLayer.data_layer(relationship.source)
) ->
true
limit || offset || has_distinct? || has_page? ->
true
true ->
false
end
end
end
defp raise_if_parent_expr!(relationship, query, reason) do
if has_parent_expr?(relationship, query.context, query.domain) do
raise ArgumentError, "Found `parent_expr` in unsupported context: #{reason}"
end
end
@doc false
def has_parent_expr?(%{
destination: destination,
filter: filter,
sort: sort,
context: rel_context
}) do
sort =
case Ash.Actions.Sort.process(destination, sort, %{}, rel_context) do
{:ok, sort} ->
sort
_ ->
nil
end
has_parent_expr_in_sort?(sort) ||
do_has_parent_expr?(filter)
end
@doc false
def has_parent_expr?(
%{
destination: destination,
filter: filter,
source: source,
sort: sort,
context: rel_context
},
context,
domain
) do
case Ash.Actions.Sort.process(destination, sort, %{}, rel_context) do
{:ok, sort} ->
sort
_ ->
nil
end
parent_stack = [source | Ash.Actions.Read.parent_stack_from_context(context)]
has_parent_expr_in_sort?(sort) ||
filter
|> Ash.Filter.hydrate_refs(%{
parent_stack: parent_stack,
resource: destination,
public?: false
})
|> Ash.Actions.Read.add_calc_context_to_filter(
context[:private][:actor],
context[:private][:authorize],
context[:private][:tenant],
context[:private][:tracer],
domain,
destination,
expand?: true,
parent_stack: parent_stack
)
|> do_has_parent_expr?()
end
defp has_parent_expr_in_sort?(sort) do
sort
|> List.wrap()
|> Enum.any?(fn
atom when is_atom(atom) ->
false
{atom, _} when is_atom(atom) ->
false
%Ash.Query.Calculation{} = calculation ->
expression = calculation.module.expression(calculation.opts, calculation.context)
do_has_parent_expr?(expression)
{%Ash.Query.Calculation{} = calculation, _} ->
expression = calculation.module.expression(calculation.opts, calculation.context)
do_has_parent_expr?(expression)
end)
end
@doc false
def do_has_parent_expr?(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
do_has_parent_expr?(expr, depth - 1)
end
%Ash.Query.Exists{expr: expr} ->
do_has_parent_expr?(expr, depth + 1)
%Ash.Query.Parent{expr: expr} ->
if depth == 0 do
true
else
do_has_parent_expr?(expr, depth - 1)
end
%Ash.Query.Ref{
attribute: %Ash.Query.Aggregate{
field: %Ash.Query.Calculation{module: module, opts: opts, context: context}
}
} ->
if module.has_expression?() do
module.expression(opts, context)
|> do_has_parent_expr?(depth + 1)
else
false
end
_other ->
false
end,
true,
true,
true
)
)
end
defp offset_and_limit(query) do
if query.offset == 0 do
{nil, query.limit}
else
{query.offset, query.limit}
end
end
defp is_many_to_many_not_unique_on_join?(
%{type: :many_to_many} = relationship,
query,
source_query
) do
keys =
[
relationship.source_attribute_on_join_resource,
relationship.destination_attribute_on_join_resource
]
join_keys =
if query.tenant && source_query.tenant &&
Ash.Resource.Info.multitenancy_strategy(relationship.through) == :attribute do
attr = Ash.Resource.Info.multitenancy_attribute(relationship.through)
[attr | keys]
else
keys
end
primary_key_is_join_keys? =
Enum.all?(
Ash.Resource.Info.primary_key(relationship.through),
&(&1 in join_keys)
)
is_unique_on_join_keys? =
Enum.any?(Ash.Resource.Info.identities(relationship.through), fn identity ->
is_nil(identity.where) && identity.nils_distinct? &&
Enum.all?(identity.keys, &(&1 in join_keys))
end)
not (primary_key_is_join_keys? || is_unique_on_join_keys?)
end
defp is_many_to_many_not_unique_on_join?(_, _, _), do: false
end