Packages
ash
2.19.0
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.
Retired package: Release invalid - soft destroys not honored in some conditions, possible data loss can occur
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
def load([], _query, _lazy?) do
{:ok, []}
end
def load(record, query, lazy?) when not is_list(record) do
case load([record], query, lazy?) do
{:ok, [record]} -> {:ok, record}
{:error, error} -> {:error, error}
end
end
def load(records, %{load: load}, _lazy?) when load in [%{}, [], nil] do
{:ok, records}
end
def load(records, query, lazy?) do
query.load
|> with_related_queries(query, records, lazy?)
|> fetch_related_records(records)
|> 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, error}}, _ ->
{:halt, {:error, Ash.Error.set_path(error, relationship.name)}}
end)
end
defp fetch_related_records(relationships_and_queries, records) do
Enum.map(relationships_and_queries, fn
{relationship, {:lazy, query}} ->
{relationship, {:lazy, query}, lazy_related_records(records, relationship, query)}
{relationship, %{valid?: true} = related_query} ->
do_fetch_related_records(records, relationship, related_query)
{relationship, %{errors: errors} = related_query} ->
{relationship, related_query, {:error, errors}}
end)
|> Ash.Actions.Read.AsyncLimiter.await_all()
end
defp lazy_related_records(records, relationship, related_query) 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)
related_query.api.load(related_records_with_lazy_join_source, related_query,
lazy?: true,
actor: related_query.context.private[:actor],
tenant: related_query.tenant,
authorize?: related_query.context.private[:authorize?]
)
end
defp with_related_queries(load, query, records, lazy?) do
Stream.map(load, fn {relationship_name, related_query} ->
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
{relationship,
{:lazy,
related_query
|> Map.put(:api, query.api)
|> Ash.Query.set_context(%{
private: %{async_limiter: query.context[:private][:async_limiter]}
})}}
else
related_query(relationship_name, records, related_query, query)
end
end)
end
defp related_query(relationship_name, records, related_query, query) do
relationship = Ash.Resource.Info.relationship(query.resource, relationship_name)
read_action_name =
relationship.read_action ||
Ash.Resource.Info.primary_action!(relationship.destination, :read).name
related_query =
related_query
|> case do
%Ash.Query{} = related_query ->
related_query
[] ->
Ash.Query.new(relationship.destination)
end
|> 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,
%{},
authorize?: query.context[:private][:authorize?],
actor: query.context[:private][:actor],
tracer: query.context[:private][:tracer]
)
|> Map.put(:api, relationship.api || query.api)
|> Ash.Query.sort(relationship.sort)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.set_context(relationship.context)
|> hydrate_refs(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, 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],
tracer: source_query.context[:private][:tracer]
)
|> 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
])
|> Map.put(:api, join_relationship.api || related_query.api)
|> hydrate_refs(relationship.source)
if source_query.context[:private][:authorize?] do
case through_query.api.can(
through_query,
source_query.context[:private][:actor],
return_forbidden_error?: true,
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, parent) do
case Ash.Filter.hydrate_refs(query.filter, %{
resource: query.resource,
parent_stack: [parent],
public?: false
}) 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(
records,
%{manual: {module, opts}} = relationship,
related_query
) do
Ash.Actions.Read.AsyncLimiter.async_or_inline(
related_query,
Ash.context_to_opts(related_query.context),
fn ->
result =
module.load(records, opts, %{
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}
}),
actor: related_query.context[:private][:actor],
authorize?: related_query.context[:private][:authorize?],
api: related_query.api,
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)
|> related_query.api.load(related_query,
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(
_records,
%{no_attributes?: true} = relationship,
related_query
) do
Ash.Actions.Read.AsyncLimiter.async_or_inline(
related_query,
Ash.context_to_opts(related_query.context),
fn ->
result =
related_query
|> select_destination_attribute(relationship)
|> Ash.Query.set_context(%{
accessing_from: %{source: relationship.source, name: relationship.name}
})
|> Ash.Actions.Read.unpaginated_read()
{relationship, related_query, result}
end
)
end
defp do_fetch_related_records(
_records,
relationship,
%{context: %{data_layer: %{lateral_join_source: {_, _}}}} = related_query
) do
Ash.Actions.Read.AsyncLimiter.async_or_inline(
related_query,
Ash.context_to_opts(related_query.context),
fn ->
result =
related_query
|> select_destination_attribute(relationship)
|> Ash.Query.set_context(%{
accessing_from: %{source: relationship.source, name: relationship.name}
})
|> Ash.Actions.Read.unpaginated_read()
{relationship, related_query, result}
end
)
end
defp do_fetch_related_records(records, %{type: :many_to_many} = relationship, related_query) 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.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
])
|> Map.put(:api, join_relationship.api || related_query.api)
Ash.Actions.Read.AsyncLimiter.async_or_inline(
related_query,
Ash.context_to_opts(related_query.context),
fn ->
case Ash.Actions.Read.unpaginated_read(join_query, nil,
authorize?: related_query.context[:private][:authorize?],
actor: related_query.context[:private][:actor],
tracer: related_query.context[:private][:tracer]
) 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
|> 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.Actions.Read.unpaginated_read()
|> 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(records, relationship, related_query) do
destination_attributes = Enum.map(records, &Map.get(&1, relationship.source_attribute))
Ash.Actions.Read.AsyncLimiter.async_or_inline(
related_query,
Ash.context_to_opts(related_query.context),
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])
|> Ash.Query.set_context(%{
accessing_from: %{source: relationship.source, name: relationship.name}
})
|> Ash.Actions.Read.unpaginated_read()
{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, :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(
[%resource{} | _] = records,
relationship,
related_records,
%{context: %{data_layer: %{lateral_join_source: {_, _}}}}
) do
source_attribute =
Ash.Resource.Info.attribute(relationship.source, relationship.source_attribute)
pkey_simple_equality? = Ash.Resource.Info.primary_key_simple_equality?(relationship.source)
source_attribute_simple_equality? = 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
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
Map.put(record, relationship.name, default)
else
{:ok, value} ->
Map.put(record, relationship.name, value)
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
])
Map.put(record, relationship.name, related)
end)
end
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}}})
)
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)
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
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)
Map.put(
record,
relationship.name,
apply_runtime_query_operations(
Map.get(related, value) || default,
related_query
)
)
end)
else
Enum.map(records, fn record ->
value = Map.get(record, relationship.source_attribute)
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(result, related_query)
put_result(record, result, relationship.name, default)
end)
end)
end
end
defp apply_runtime_query_operations({:ok, value}, related_query) do
{:ok, apply_runtime_query_operations(value, related_query)}
end
defp apply_runtime_query_operations(:error, _related_query), do: :error
defp apply_runtime_query_operations(empty, _related_query) when empty in [nil, []], do: empty
defp apply_runtime_query_operations(value, related_query) when not is_list(value) do
[value] |> apply_runtime_query_operations(related_query) |> Enum.at(0)
end
defp apply_runtime_query_operations(value, related_query) do
value
|> apply_runtime_offset(related_query)
# |> apply_runtime_distinct(related_query)
|> apply_runtime_limit(related_query)
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 lateral_join?(%{action: action} = query, relationship, source_data) do
if action.manual do
raise_if_parent_expr!(relationship, "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]
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 ->
has_parent_expr?(relationship)
limit == 1 && (source_data == :unknown || Enum.count_until(source_data, 2) == 1) &&
relationship.type != :many_to_many ->
has_parent_expr?(relationship)
has_parent_expr?(relationship) ->
true
!Ash.DataLayer.data_layer_can?(
relationship.source,
{:lateral_join, resources}
) ->
false
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? ->
true
true ->
false
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
@doc false
def has_parent_expr?(%{destination: destination, filter: filter, sort: sort, context: context}) do
{:ok, sort} = Ash.Actions.Sort.process(destination, sort, %{}, context)
do_has_parent_expr?(filter) || has_parent_expr_in_sort?(sort)
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
_ ->
false
end)
)
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) do
join_keys =
Enum.sort([
relationship.source_attribute_on_join_resource,
relationship.destination_attribute_on_join_resource
])
primary_key_is_join_keys? =
Enum.sort(Ash.Resource.Info.primary_key(relationship.through)) == join_keys
is_unique_on_join_keys? =
Enum.any?(Ash.Resource.Info.identities(relationship.through), fn identity ->
Enum.sort(identity.keys) == 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