Packages
ash
2.14.10
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/load.ex
defmodule Ash.Actions.Load do
@moduledoc false
alias Ash.Engine.Request
require Ash.Query
def requests(
query,
lazy?,
opts,
request_path,
root_query \\ nil,
path \\ [],
tenant \\ nil
)
def requests(nil, _, _opts, _, _, _, _), do: []
def requests(%{load: []}, _, _opts, _, _, _, _), do: []
def requests(
%{load: loads} = query,
lazy?,
opts,
request_path,
root_query,
path,
tenant
) do
root_query = root_query || query
tenant = tenant || query.tenant || root_query.tenant
loads
|> List.wrap()
|> Enum.flat_map(fn {relationship, further} ->
relationship = Ash.Resource.Info.relationship(query.resource, relationship)
related_query =
case further do
%Ash.Query{} = query ->
Ash.Query.set_api(query, relationship.api || root_query.api)
further ->
relationship.destination
|> Ash.Query.new(root_query.api)
|> Ash.Query.load(further)
end
related_query =
if tenant && !related_query.tenant do
Ash.Query.set_tenant(related_query, tenant)
else
related_query
end
related_query =
if Map.get(relationship, :no_attributes?) do
related_query
else
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
related_query =
if relationship.cardinality == :one do
Ash.Query.limit(related_query, 1)
else
related_query
end
new_path = [relationship | path]
requests(
related_query,
lazy?,
opts,
request_path,
root_query,
new_path,
related_query.tenant
) ++
do_requests(
relationship,
lazy?,
opts,
request_path,
related_query,
path,
root_query
)
end)
end
def attach_loads([%resource{} | _] = data, loads) do
loads
|> Enum.sort_by(fn {key, _value} ->
last_relationship = last_relationship!(resource, key)
# We want to do `many_to_many` last, so we know we've
# done their join assocs first. Pretty hacky
{length(key), last_relationship.type == :many_to_many}
end)
|> Enum.reduce(data, fn {key, %{data: value}}, data ->
last_relationship = last_relationship!(resource, key)
lead_path = :lists.droplast(key)
case last_relationship do
%{type: :many_to_many} ->
attach_many_to_many_loads(
data,
lead_path,
last_relationship,
loads,
value
)
%{cardinality: :many} ->
attach_to_many_loads(value, last_relationship, data, lead_path)
%{cardinality: :one} ->
attach_to_one_loads(value, last_relationship, data, lead_path)
end
end)
end
def attach_loads(data, state) when not is_list(data) do
[data]
|> attach_loads(state)
|> List.first()
end
def attach_loads(data, _state) do
data
end
defp attach_to_many_loads(
value,
%{name: name, no_attributes?: true} = relationship,
data,
lead_path
) do
value =
case value do
%_{} = value ->
List.wrap(value)
list when is_list(list) ->
list
value when is_map(value) ->
value |> Map.values() |> List.flatten()
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)
when is_map(value) do
if Ash.Resource.Info.primary_key_simple_equality?(last_relationship.destination) do
primary_key = Ash.Resource.Info.primary_key(last_relationship.source)
map_or_update(data, lead_path, fn record ->
case primary_key do
[field] ->
Map.put(
record,
last_relationship.name,
Map.get(value, Map.take(record, primary_key)) ||
Map.get(value, Map.get(record, field)) || []
)
_ ->
Map.put(record, last_relationship.name, Map.get(value, Map.take(record, primary_key)))
end
end)
else
primary_key = Ash.Resource.Info.primary_key(last_relationship.source)
value = Map.to_list(value)
single_primary_key =
case primary_key do
[field] ->
field
_ ->
nil
end
map_or_update(data, lead_path, fn record ->
related =
value
|> Enum.find_value(fn {key, value} ->
key =
if is_map(key) || !single_primary_key do
key
else
%{single_primary_key => key}
end
if last_relationship.source.primary_key_matches?(record, key) do
value
end
end)
Map.put(
record,
last_relationship.name,
related
)
end)
end
end
defp attach_to_many_loads(value, last_relationship, data, lead_path) do
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
record ->
Map.get(record, last_relationship.destination_attribute)
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} = 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
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
:error ->
case value do
%{__lateral_join_source__: destination_value} when not is_nil(destination_value) ->
source_value ==
destination_value
value ->
Map.put(record, name, value |> List.wrap() |> Enum.at(0))
end
end
end
end)
end
defp attach_to_one_loads(value, last_relationship, data, lead_path) when is_map(value) do
primary_key = Ash.Resource.Info.primary_key(last_relationship.source)
single_primary_key =
case primary_key do
[field] ->
field
_ ->
nil
end
map_or_update(data, lead_path, fn record ->
related =
Enum.find_value(value, fn {key, value} ->
key =
if is_map(key) || !single_primary_key do
key
else
%{single_primary_key => key}
end
if last_relationship.source.primary_key_matches?(record, key) do
{:ok, value}
end
end)
|> case do
{:ok, value} ->
value
_ ->
nil
end
Map.put(
record,
last_relationship.name,
related
)
end)
end
defp attach_to_one_loads(value, last_relationship, data, lead_path) do
destination_attribute = last_relationship.destination_attribute
type = Ash.Resource.Info.attribute(last_relationship.destination, destination_attribute).type
if Ash.Type.simple_equality?(type) do
values =
value
|> Enum.reverse()
|> Enum.into(%{}, fn item ->
{Map.get(item, last_relationship.destination_attribute), item}
end)
map_or_update(data, lead_path, fn record ->
source_key = Map.get(record, last_relationship.source_attribute)
related_record = Map.get(values, source_key)
Map.put(record, last_relationship.name, related_record)
end)
else
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 ->
Map.get(maybe_related, destination_attribute) == source_key
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
defp attach_many_to_many_loads(data, lead_path, last_relationship, loads, value) do
join_path = lead_path ++ [last_relationship.join_relationship]
join_data =
loads
|> Map.get(join_path, %{})
|> Map.get(:data, [])
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 =
case relationship_type_with_non_simple_equality(join_relationship) do
{:ok, type} ->
join_data
|> Enum.filter(fn join_row ->
Ash.Type.equal?(
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))
: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)
end
defp map_or_update(nil, _, _), do: nil
defp map_or_update(record, [], func) when not is_list(record), do: func.(record)
defp map_or_update(records, [], func) do
Enum.map(records, fn record ->
if record do
func.(record)
else
nil
end
end)
end
defp map_or_update(records, [path | tail], func) do
map_or_update(records, [], fn record ->
Map.update!(record, path, &map_or_update(&1, tail, func))
end)
end
defp last_relationship!(resource, [last]) do
Ash.Resource.Info.relationship(resource, last) || raise "Assumption Failed"
end
defp last_relationship!(resource, [first | rest]) do
relationship = Ash.Resource.Info.relationship(resource, first) || raise "Assumption Failed"
last_relationship!(relationship.destination, rest)
end
defp do_requests(relationship, lazy?, opts, request_path, related_query, path, root_query) do
join_assoc_request =
if relationship.type == :many_to_many &&
!lateral_join?(related_query, relationship, :unknown) do
join_assoc_request(
relationship,
request_path,
related_query,
root_query,
path,
opts,
lazy?
)
end
join_request_path =
if join_assoc_request do
join_assoc_request.path
end
load_request =
load_request(
relationship,
lazy?,
opts,
request_path,
related_query,
root_query,
path,
join_request_path
)
if join_assoc_request do
[join_assoc_request, load_request]
else
[load_request]
end
end
@doc false
def calc_dep_requests(
relationship,
opts,
calc_dep,
path,
root_query,
select,
load
) do
join_assoc_request =
if relationship.type == :many_to_many &&
!lateral_join?(calc_dep.query, relationship, :unknown) do
calc_dep_join_assoc_request(
relationship,
path,
root_query,
path,
opts,
calc_dep
)
end
load_request =
calc_dep_load_request(
relationship,
opts,
calc_dep,
root_query,
path,
select,
load
)
if join_assoc_request do
[join_assoc_request, load_request]
else
[load_request]
end
end
defp calc_dep_load_request(
relationship,
opts,
calc_dep,
root_query,
path,
select,
load
) do
{parent_dep_path, parent_data_path, parent_query_path} =
case calc_dep.path do
[] ->
{path ++ [:fetch, :data], path ++ [:fetch, :data, :results], path ++ [:fetch, :query]}
dependent_path ->
{relationship, query} = List.last(dependent_path)
depended_dep = %{
type: :relationship,
path: :lists.droplast(dependent_path),
query: query,
relationship: relationship
}
data = path ++ [:calc_dep, depended_dep, :data]
{data, data, path ++ [:calc_dep, depended_dep, :query]}
end
this_request_path = path ++ [:calc_dep, calc_dep]
dependencies = [
this_request_path ++ [:authorization_filter],
parent_dep_path,
parent_query_path
]
dependencies =
if relationship.type == :many_to_many &&
!lateral_join?(calc_dep.query, relationship, :unknown) do
dependencies ++ [this_request_path ++ [:join, :data]]
else
dependencies
end
rel_string =
calc_dep.path
|> Enum.map(&elem(&1, 0))
|> Enum.concat([calc_dep.relationship])
|> Enum.join(".")
actual_query =
calc_dep.query
|> Ash.Query.select(select)
|> Ash.Query.set_context(%{
accessing_from: %{source: relationship.source, name: relationship.name}
})
|> Ash.Query.load(load)
Request.new(
action:
calc_dep.query.action || Ash.Resource.Info.primary_action(relationship.destination, :read),
resource: relationship.destination,
name: "calc dep #{rel_string}",
api: calc_dep.query.api,
path: this_request_path,
query: actual_query,
data:
calc_dep_data(
relationship,
dependencies,
this_request_path,
calc_dep,
path,
root_query,
opts,
parent_data_path,
parent_query_path,
this_request_path ++ [:join],
nil,
actual_query
)
)
end
defp load_request(
relationship,
lazy?,
opts,
request_path,
related_query,
root_query,
path,
join_request_path
) do
relationship_path = Enum.reverse(Enum.map([relationship | path], &Map.get(&1, :name)))
this_request_path =
request_path ++
[
:load,
relationship_path
]
dependencies =
case path do
[] ->
[this_request_path ++ [:authorization_filter]]
dependent_path ->
[
this_request_path ++ [:authorization_filter],
request_path ++
[:load, Enum.reverse(Enum.map(dependent_path, &Map.get(&1, :name))), :data],
request_path ++
[:load, Enum.reverse(Enum.map(dependent_path, &Map.get(&1, :name))), :query]
]
end
dependencies = [request_path ++ [:data] | dependencies]
dependencies =
if relationship.type == :many_to_many &&
!lateral_join?(related_query, relationship, :unknown) do
join_relationship = join_relationship(relationship)
join_relationship_path =
Enum.map(join_relationship_path(path, join_relationship), & &1.name)
[
request_path ++
[
:load,
join_relationship_path,
:data
]
| dependencies
]
else
dependencies
end
source =
[relationship | path]
|> Enum.reverse()
|> Enum.map_join(".", &Map.get(&1, :name))
related_query =
Ash.Query.set_context(related_query, %{
accessing_from: %{source: relationship.source, name: relationship.name}
})
Request.new(
action:
related_query.action || Ash.Resource.Info.primary_action(relationship.destination, :read),
resource: relationship.destination,
name: "load #{source}",
api: related_query.api,
path: this_request_path,
query: related_query,
data:
data(
relationship,
lazy?,
dependencies,
this_request_path,
request_path,
related_query,
path,
root_query,
opts,
join_request_path
)
)
end
defp calc_dep_data(
%{manual: manual} = relationship,
dependencies,
_this_request_path,
calc_dep,
_path,
root_query,
request_opts,
parent_data_path,
_parent_query_path,
_join_request_path,
_,
actual_query
)
when not is_nil(manual) do
{mod, opts} =
case manual do
{mod, opts} ->
{mod, opts}
mod ->
{mod, []}
end
Request.resolve(dependencies, fn data ->
data = get_in(data, parent_data_path)
data
|> mod.load(opts, %{
relationship: relationship,
query: actual_query,
actor: request_opts[:actor],
authorize?: request_opts[:authorize?],
api: calc_dep.query.api,
tenant: root_query.tenant
})
|> case do
{:ok, result} ->
# TODO: this will result in quite a few requests potentially for aggs/calcs
# This should be optimized.
Enum.reduce_while(result, {:ok, %{}}, fn {key, records}, {:ok, acc} ->
case calc_dep.query.api.load(records, %{actual_query | load: []},
lazy?: true,
tenant: calc_dep.query.tenant,
actor: request_opts[:actor],
authorize?: request_opts[:authorize?],
tracer: request_opts[:tracer]
) do
{:ok, results} ->
{:cont, {:ok, Map.put(acc, key, results)}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
{:error, error} ->
{:error, error}
end
end)
end
defp calc_dep_data(
relationship,
dependencies,
this_request_path,
calc_dep,
path,
_root_query,
opts,
parent_data_path,
parent_query_path,
join_request_path,
non_join_source_data_path,
actual_query
) do
Request.resolve(dependencies, fn data ->
base_query =
case get_in(data, this_request_path ++ [:authorization_filter]) do
nil ->
actual_query
authorization_filter ->
Ash.Query.filter(actual_query, ^authorization_filter)
end
source_query =
data
|> get_in(parent_query_path)
|> maybe_set_calc_query_tenant(calc_dep)
with {:ok, new_query} <-
true_load_query(
relationship,
base_query,
data,
parent_data_path,
join_request_path
),
new_query <- maybe_set_calc_query_tenant(new_query, calc_dep),
{:ok, results} <-
run_actual_query(
new_query,
base_query,
data,
path,
relationship,
source_query,
opts,
false,
parent_data_path,
join_request_path,
non_join_source_data_path
) do
{:ok, results}
else
:nothing ->
{:ok, []}
{:error, error} ->
{:error, error}
end
end)
end
defp maybe_set_calc_query_tenant(query, calc_dep) do
if calc_dep.query.tenant do
Ash.Query.set_tenant(query, calc_dep.query.tenant)
else
query
end
end
defp data(
%{manual: manual} = relationship,
lazy?,
dependencies,
_this_request_path,
request_path,
related_query,
path,
root_query,
request_opts,
_join_request_path
)
when not is_nil(manual) do
{mod, opts} =
case manual do
{mod, opts} ->
{mod, opts}
mod ->
{mod, []}
end
Request.resolve(dependencies, fn data ->
data =
case path do
[] ->
get_in(data, request_path ++ [:data, :results])
path ->
data =
data
|> get_in(request_path)
|> Kernel.||(%{})
|> Map.get(:load, %{})
|> Map.get(Enum.reverse(Enum.map(path, & &1.name)), %{})
|> Map.get(:data, %{})
if is_map(data) do
data
|> Map.values()
|> Enum.flat_map(&List.wrap/1)
else
data
end
end
lazy_load_or(
data,
lazy?,
relationship.name,
related_query.api,
related_query,
request_opts,
fn ->
data
|> mod.load(opts, %{
relationship: relationship,
query: related_query,
root_query: root_query,
actor: request_opts[:actor],
authorize?: request_opts[:authorize?],
api: related_query.api,
tenant: related_query.tenant
})
|> case do
{:ok, result} ->
# TODO: this will result in quite a few requests potentially for aggs/calcs
# This should be optimized.
Enum.reduce_while(result, {:ok, %{}}, fn {key, records}, {:ok, acc} ->
case related_query.api.load(records, %{related_query | load: []},
lazy?: true,
tenant: related_query.tenant,
actor: request_opts[:actor],
authorize?: request_opts[:authorize?],
tracer: request_opts[:tracer]
) do
{:ok, results} ->
{:cont, {:ok, Map.put(acc, key, results)}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
{:error, error} ->
{:error, error}
end
end
)
end)
end
defp data(
relationship,
lazy?,
dependencies,
this_request_path,
request_path,
related_query,
path,
root_query,
opts,
join_request_path
) do
Request.resolve(dependencies, fn data ->
base_query =
case get_in(data, this_request_path ++ [:authorization_filter]) do
nil ->
related_query
authorization_filter ->
Ash.Query.filter(related_query, ^authorization_filter)
end
source_query =
case path do
[] ->
root_query
path ->
get_in(
data,
request_path ++
[
:load,
Enum.reverse(Enum.map(path, &Map.get(&1, :name))),
:query
]
)
end
source_query =
if related_query.tenant do
Ash.Query.set_tenant(source_query, related_query.tenant)
else
source_query
end
data_path =
if relationship.type == :many_to_many &&
!lateral_join?(source_query, relationship, :unknown) do
join_relationship = join_relationship(relationship)
join_relationship_path(path, join_relationship) |> Enum.map(& &1.name)
else
path |> Enum.reverse() |> Enum.map(& &1.name)
end
source_data_path =
case path do
[] ->
request_path ++ [:data, :results]
_path ->
request_path ++ [:load] ++ [data_path] ++ [:data]
end
non_join_source_data_path =
case path do
[] ->
request_path ++ [:data, :results]
_path ->
request_path ++ [:load] ++ [path |> Enum.reverse() |> Enum.map(& &1.name)] ++ [:data]
end
with {:ok, new_query} <-
true_load_query(
relationship,
base_query,
data,
source_data_path,
join_request_path
),
{:ok, results} <-
run_actual_query(
new_query,
base_query,
data,
path,
relationship,
source_query,
opts,
lazy?,
source_data_path,
join_request_path,
non_join_source_data_path
) do
{:ok, results}
else
:nothing ->
{:ok, []}
{:error, error} ->
{:error, error}
end
end)
end
defp lazy_load_or(data, lazy?, relationship, api, related_query, request_opts, func) do
if lazy? && Ash.Resource.loaded?(data, relationship) do
pkey = Ash.Resource.Info.primary_key(related_query.resource)
data
|> get_related(relationship)
|> Enum.uniq_by(&Map.take(&1, pkey))
|> api.load(related_query,
lazy?: true,
authorize?: request_opts[:authorize?],
actor: request_opts[:actor]
)
else
func.()
end
end
defp get_related(data, relationship) when is_list(data) do
Enum.flat_map(data, fn item ->
item
|> Map.get(relationship)
|> List.wrap()
end)
end
defp get_related(data, relationship) do
get_related(List.wrap(data), relationship)
end
defp join_relationship(relationship) do
Ash.Resource.Info.relationship(relationship.source, relationship.join_relationship)
end
defp join_relationship_path(path, join_relationship) do
Enum.reverse([join_relationship | path])
end
defp join_assoc_request(
relationship,
request_path,
related_query,
root_query,
path,
opts,
lazy?
) do
join_relationship = join_relationship(relationship)
join_relationship_path = join_relationship_path(path, join_relationship)
join_relationship_path_names = Enum.map(join_relationship_path, & &1.name)
dependencies =
if path == [] do
[
[:load, join_relationship_path_names, :authorization_filter]
]
else
[
[:load, join_relationship_path_names, :authorization_filter],
[:load, Enum.reverse(Enum.map(path, &Map.get(&1, :name))), :data],
[:load, Enum.reverse(Enum.map(path, &Map.get(&1, :name))), :query]
]
end
dependencies = [[:data] | dependencies]
related_query =
if related_query.tenant do
join_relationship.destination
|> Ash.Query.new(related_query.api)
|> Ash.Query.set_tenant(related_query.tenant)
else
Ash.Query.new(join_relationship.destination, related_query.api)
end
related_query =
Ash.Query.set_context(related_query, %{
accessing_from: %{source: join_relationship.source, name: join_relationship.name}
})
dependencies = Enum.map(dependencies, &(request_path ++ &1))
Request.new(
action:
related_query.action ||
Ash.Resource.Info.primary_action(relationship.destination, :read),
resource: relationship.through,
name: "load join #{join_relationship.name}",
api: related_query.api,
path: request_path ++ [:load, join_relationship_path_names],
query: related_query,
data:
Request.resolve(dependencies, fn
data ->
base_query =
case get_in(
data,
request_path ++
[
:load,
join_relationship_path_names,
:authorization_filter
]
) do
nil ->
related_query
authorization_filter ->
Ash.Query.filter(related_query, ^authorization_filter)
end
source_data =
case path do
[] ->
get_in(data, request_path ++ [:data, :results])
path ->
data
|> get_in(request_path)
|> Kernel.||(%{})
|> Map.get(:load, %{})
|> Map.get(Enum.reverse(Enum.map(path, & &1.name)), %{})
|> Map.get(:data, %{})
end
lateral_join? = lateral_join?(related_query, relationship, source_data)
base_query =
if related_query.tenant do
Ash.Query.set_tenant(base_query, related_query.tenant)
else
base_query
end
source_query =
case path do
[] ->
root_query
path ->
get_in(
data,
request_path ++
[:load, Enum.reverse(Enum.map(path, &Map.get(&1, :name))), :query]
)
end
source_query =
if related_query.tenant do
Ash.Query.set_tenant(source_query, related_query.tenant)
else
source_query
end
data_path = path |> Enum.reverse() |> Enum.map(& &1.name)
source_data_path =
case path do
[] ->
request_path ++ [:data, :results]
_path ->
request_path ++ [:load] ++ [data_path] ++ [:data]
end
with {:ok, new_query} <-
true_load_query(
join_relationship,
base_query,
data,
source_data_path,
nil
),
new_query <-
add_join_destination_filter(
new_query,
lateral_join?,
data,
relationship,
[
:load,
Enum.reverse(Enum.map(path, &Map.get(&1, :name))) ++ [relationship.name],
:data
]
),
{:ok, results} <-
run_actual_query(
new_query,
base_query,
data,
path,
join_relationship,
source_query,
opts,
lazy?,
source_data_path,
nil,
nil
) do
{:ok, results}
else
:nothing ->
{:ok, []}
{:error, error} ->
{:error, error}
end
end)
)
end
defp calc_dep_join_assoc_request(
relationship,
path,
root_query,
path,
opts,
calc_dep
) do
{parent_dep_path, parent_data_path, parent_query_path} =
case calc_dep.path do
[] ->
{path ++ [:fetch], path ++ [:fetch, :data, :results], path ++ [:fetch, :query]}
dependent_path ->
{relationship, query} = List.last(dependent_path)
depended_dep = %{
type: :relationship,
path: :lists.droplast(dependent_path),
query: query,
relationship: relationship
}
{path ++ [:calc_dep, depended_dep], path ++ [:calc_dep, depended_dep, :data, :results],
path ++ [:calc_dep, depended_dep, :query]}
end
this_request_path = path ++ [:calc_dep, calc_dep]
join_relationship = join_relationship(relationship)
dependencies = [
this_request_path ++ [:authorization_filter],
parent_dep_path ++ [:data],
parent_dep_path ++ [:query]
]
base_query =
join_relationship.destination
|> Ash.Query.new(join_relationship.api || root_query.api)
|> Ash.Query.set_tenant(calc_dep.query.tenant)
|> Ash.Query.set_context(join_relationship.context)
|> Ash.Query.set_context(%{
accessing_from: %{source: join_relationship.source, name: join_relationship.name}
})
Request.new(
action:
calc_dep.query.action ||
Ash.Resource.Info.primary_action(join_relationship.destination, :read),
resource: relationship.through,
name: "load calc_dep join #{join_relationship.name}",
api: calc_dep.query.api,
path: path ++ [:calc_dep, calc_dep, :join],
query: base_query,
data:
Request.resolve(dependencies, fn
data ->
base_query =
case get_in(
data,
this_request_path ++ [:authorization_filter]
) do
nil ->
base_query
authorization_filter ->
base_query
|> Ash.Query.do_filter(authorization_filter, parent_stack: relationship.source)
end
source_data = get_in(data, parent_data_path)
lateral_join? = lateral_join?(calc_dep.query, relationship, source_data)
source_query =
get_in(
data,
parent_query_path
)
with {:ok, new_query} <-
true_load_query(
join_relationship,
base_query,
data,
parent_data_path,
nil
),
new_query <-
add_join_destination_filter(
new_query,
lateral_join?,
data,
relationship,
parent_data_path
),
{:ok, results} <-
run_actual_query(
new_query,
base_query,
data,
path,
relationship,
source_query,
opts,
false,
parent_data_path,
nil,
nil
) do
{:ok, results}
else
:nothing ->
{:ok, []}
{:error, error} ->
{:error, error}
end
end)
)
end
defp add_join_destination_filter(query, true, data, relationship, destination_path) do
ids =
data
|> get_in(destination_path)
|> case do
%page{results: results} when page in [Ash.Page.Keyset, Ash.Page.Offset] ->
results
data ->
data
end
|> List.wrap()
|> Enum.map(fn related ->
Map.get(related, relationship.destination_attribute)
end)
filter = [{relationship.destination_attribute_on_join_resource, [{:in, ids}]}]
Ash.Query.filter(query, ^filter)
end
defp add_join_destination_filter(query, false, _, _, _) do
query
end
defp lateral_join?(query, relationship, source_data) do
action =
case relationship.read_action do
nil ->
Ash.Resource.Info.primary_action!(relationship.destination, :read)
action ->
Ash.Resource.Info.action(relationship.destination, action)
end
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)
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)
true ->
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}
)
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([
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
defp run_actual_query(
query,
base_query,
data,
path,
relationship,
source_query,
request_opts,
lazy?,
source_data_path,
join_request_path,
non_join_source_data
) do
{offset, limit} = offset_and_limit(base_query)
source_data =
case get_in(data, source_data_path) do
value when is_map(value) ->
value
|> Map.values()
|> Enum.flat_map(&List.wrap/1)
value ->
value
end
source_data_to_get_lazy_load_from =
if relationship.type == :many_to_many && lazy? do
case get_in(data, non_join_source_data) do
value when is_map(value) ->
value
|> Map.values()
|> Enum.flat_map(&List.wrap/1)
value ->
value
end
else
source_data
end
lazy_load_or(
source_data_to_get_lazy_load_from,
lazy?,
relationship.name,
query.api,
query,
request_opts,
fn ->
cond do
lateral_join?(query, relationship, source_data) && relationship.type == :many_to_many ->
join_relationship =
Ash.Resource.Info.relationship(relationship.source, relationship.join_relationship)
query
|> Ash.Query.set_context(%{
data_layer: %{
lateral_join_source: {
source_data,
[
{source_query, relationship.source_attribute,
relationship.source_attribute_on_join_resource, relationship},
{relationship.through, relationship.destination_attribute_on_join_resource,
relationship.destination_attribute, join_relationship}
]
}
}
})
|> Ash.Query.set_context(relationship.context)
|> 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 || has_parent_expr?(relationship)) ->
query
|> Ash.Query.set_context(%{
data_layer: %{
lateral_join_source:
{source_data,
[
{source_query, relationship.source_attribute,
relationship.destination_attribute, relationship}
]}
}
})
|> Ash.Query.set_context(relationship.context)
|> 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)
limit || offset ->
artificial_limit_and_offset(
query,
limit,
offset,
relationship,
request_opts,
source_data,
path,
data,
join_request_path
)
true ->
query
|> Ash.Query.set_context(relationship.context)
|> 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
)
end
end
)
end
defp offset_and_limit(query) do
if query.offset == 0 do
{nil, query.limit}
else
{query.offset, query.limit}
end
end
defp artificial_limit_and_offset(
query,
limit,
offset,
relationship,
request_opts,
_source_data,
_path,
data,
join_request_path
) do
query
|> Ash.Query.set_context(relationship.context)
|> 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)
|> case do
{:ok, results} ->
new_results =
if relationship.type == :many_to_many do
results = Enum.with_index(results)
join_data = get_in(data, join_request_path ++ [:data])
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(
&Map.take(&1, [
relationship.source_attribute_on_join_resource,
relationship.destination_attribute_on_join_resource
])
)
|> Enum.group_by(&Map.get(&1, relationship.source_attribute_on_join_resource))
|> Enum.flat_map(fn {_, group} ->
group =
Enum.map(group, fn join_row ->
Enum.find_value(results, fn {record, index} ->
if equality.(
Map.get(join_row, relationship.destination_attribute_on_join_resource),
Map.get(record, relationship.destination_attribute)
) do
{Map.put(
record,
:__lateral_join_source__,
Map.get(join_row, relationship.source_attribute_on_join_resource)
), index}
end
end)
end)
offset_records =
group
|> Enum.sort_by(&elem(&1, 1))
|> Enum.map(&elem(&1, 0))
|> Enum.drop(offset || 0)
if limit do
Enum.take(offset_records, limit)
else
offset_records
end
end)
else
# TODO: This needs to support `Ash.Type.equal?`
results
|> Enum.with_index()
|> Enum.group_by(fn {record, _i} ->
Map.get(record, relationship.destination_attribute)
end)
|> Enum.flat_map(fn {_, group} ->
group =
group
|> Enum.sort_by(&elem(&1, 1))
|> Enum.map(&elem(&1, 0))
offset_records = Enum.drop(group, offset || 0)
if limit do
Enum.take(offset_records, limit)
else
offset_records
end
end)
end
{:ok, new_results}
{:error, error} ->
{:error, error}
end
end
defp read(query, action, request_opts) do
action = action || primary_read(query)
Ash.Actions.Read.unpaginated_read(
query,
action,
request_opts
)
end
defp primary_read(query) do
action = Ash.Resource.Info.primary_action(query.resource, :read)
if action do
action
else
raise """
No read action for loaded resource: #{query.resource}
"""
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, []] ->
query
load ->
new_load =
load
|> List.wrap()
|> Enum.reject(fn
item when is_atom(item) ->
Ash.Resource.Info.relationship(query.resource, item)
{item, _} ->
Ash.Resource.Info.relationship(query.resource, item)
end)
%{query | load: new_load}
end
end
defp true_load_query(relationship, query, data, source_data_path, join_relationship_path) do
{source_attribute, source_data_path} =
if relationship.type == :many_to_many && !lateral_join?(query, relationship, :unknown) do
{relationship.destination_attribute_on_join_resource, join_relationship_path ++ [:data]}
else
{relationship.source_attribute, source_data_path}
end
case get_in(data, source_data_path) do
%{data: empty} when empty in [[], nil] ->
:nothing
empty when empty in [[], nil] ->
:nothing
source_data ->
get_query(query, relationship, source_data, source_attribute)
end
end
defp get_query(query, relationship, source_data, source_attribute) do
{offset, limit} = offset_and_limit(query)
query = Ash.Query.ensure_selected(query, Ash.Actions.Read.source_fields(query))
cond do
lateral_join?(query, relationship, source_data) ->
{:ok, %{query | load: []}}
Map.get(relationship, :no_attributes?) ->
{:ok, %{query | load: []}}
true ->
query =
if limit || offset do
Ash.Query.unset(query, [:limit, :offset])
else
query
end
related_data =
case source_data do
%page{results: results} when page in [Ash.Page.Keyset, Ash.Page.Offset] ->
results
data ->
data
end
ids =
related_data
|> Enum.flat_map(fn
{_, data} when is_list(data) ->
Enum.map(data, &Map.get(&1, source_attribute))
{_, data} ->
data
|> Map.get(source_attribute)
|> List.wrap()
data ->
data
|> Map.get(source_attribute)
|> List.wrap()
end)
|> Enum.reject(&is_nil/1)
filter_value =
case ids do
[id] ->
id
ids ->
[in: ids]
end
new_query =
query
|> Ash.Query.filter(^[{relationship.destination_attribute, filter_value}])
|> Map.put(:load, [])
{:ok, new_query}
end
end
def reverse_relationship_path(relationship, []) do
relationship.destination
|> Ash.Resource.Info.relationships()
|> Enum.find(fn destination_relationship ->
reverse_relationship?(relationship, destination_relationship)
end)
|> case do
nil ->
:error
reverse ->
{:ok, [reverse.name]}
end
end
def reverse_relationship_path(relationship, rest) do
relationship.destination
|> Ash.Resource.Info.relationships()
|> Enum.find(fn destination_relationship ->
reverse_relationship?(relationship, destination_relationship)
end)
|> case do
nil ->
:error
reverse ->
case do_reverse_relationship_path(reverse, rest) do
:error ->
:error
{:ok, path} ->
{:ok, [reverse.name | path]}
end
end
end
def do_reverse_relationship_path(relationship, prior_path, acc \\ [])
def do_reverse_relationship_path(nil, _, _) do
:error
end
def do_reverse_relationship_path(relationship, [], acc) do
relationship.source
|> Ash.Resource.Info.relationships()
|> Enum.find(fn destination_relationship ->
reverse_relationship?(relationship, destination_relationship)
end)
|> case do
nil ->
:error
reverse ->
{:ok, [reverse.name | acc]}
end
end
def do_reverse_relationship_path(relationship, [next_relationship | rest], acc) do
relationship.destination
|> Ash.Resource.Info.relationships()
|> Enum.find(fn destination_relationship ->
reverse_relationship?(relationship, destination_relationship)
end)
|> case do
nil ->
:error
reverse ->
do_reverse_relationship_path(
Ash.Resource.Info.relationship(relationship.destination, next_relationship),
rest,
[reverse.name | acc]
)
end
end
defp reverse_relationship?(rel, destination_rel) do
rel.source == destination_rel.destination &&
rel.destination == destination_rel.source &&
rel.source_attribute == destination_rel.destination_attribute &&
rel.destination_attribute == destination_rel.source_attribute &&
Map.fetch(rel, :source_attribute_on_join_resource) ==
Map.fetch(destination_rel, :destination_attribute_on_join_resource) &&
Map.fetch(rel, :destination_attribute_on_join_resource) ==
Map.fetch(destination_rel, :source_attribute_on_join_resource) &&
is_nil(destination_rel.context) &&
is_nil(rel.context)
end
end