Packages

ash

1.48.0-rc.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.

Security advisory: This version has known vulnerabilities. View advisories

Current section

Files

Jump to
ash lib ash actions load.ex
Raw

lib/ash/actions/load.ex

defmodule Ash.Actions.Load do
@moduledoc false
alias Ash.Engine
alias Ash.Engine.Request
require Ash.Query
def requests(
query,
root_query \\ nil,
path \\ [],
tenant \\ nil
)
def requests(nil, _, _, _), do: {nil, []}
def requests(%{load: []} = query, _, _, _), do: {query, []}
def requests(
%{load: loads} = query,
root_query,
path,
tenant
) do
root_query = root_query || query
tenant = tenant || query.tenant || root_query.tenant
loads
|> List.wrap()
|> Enum.reduce({query, []}, fn {relationship, further}, {query, requests} ->
relationship = Ash.Resource.Info.relationship(query.resource, relationship)
related_query =
case further do
%Ash.Query{} = query ->
Ash.Query.set_api(query, 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
|> Ash.Query.ensure_selected(relationship.destination_field)
related_query =
if relationship.cardinality == :one do
Ash.Query.limit(related_query, 1)
else
related_query
end
new_path = [relationship | path]
{related_query, further_requests} =
requests(
related_query,
root_query,
new_path,
related_query.tenant
)
{
Ash.Query.ensure_selected(query, relationship.source_field),
requests ++
further_requests ++
do_requests(
relationship,
related_query,
path,
root_query
)
}
end)
end
def attach_loads([%resource{} | _] = data, %{load: 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, 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, _) do
data
end
defp attach_to_many_loads(value, last_relationship, data, lead_path) do
values = Enum.group_by(value, &Map.get(&1, last_relationship.destination_field))
map_or_update(data, lead_path, fn record ->
source_key = Map.get(record, last_relationship.source_field)
related_records = Map.get(values, source_key, [])
Map.put(record, last_relationship.name, related_records)
end)
end
defp attach_to_one_loads(value, last_relationship, data, lead_path) do
values =
value
|> Enum.reverse()
|> Enum.into(%{}, fn item ->
{Map.get(item, last_relationship.destination_field), item}
end)
map_or_update(data, lead_path, fn record ->
source_key = Map.get(record, last_relationship.source_field)
related_record = Map.get(values, source_key)
Map.put(record, last_relationship.name, related_record)
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_or_update(data, lead_path, fn record ->
source_value = Map.get(record, last_relationship.source_field)
join_values =
join_data
|> Enum.filter(fn join_row ->
Map.get(join_row, last_relationship.source_field_on_join_table) ==
source_value
end)
|> Enum.map(&Map.get(&1, last_relationship.destination_field_on_join_table))
related_records =
value
|> Enum.filter(fn value ->
destination_value = Map.get(value, last_relationship.destination_field)
destination_value in join_values
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, related_query, path, root_query) do
load_request =
load_request(
relationship,
related_query,
root_query,
path
)
case relationship.type do
:many_to_many ->
case join_assoc_request(
relationship,
related_query,
root_query,
path
) do
nil ->
[load_request]
request ->
[load_request, request]
end
_ ->
[load_request]
end
end
defp load_request(
relationship,
related_query,
root_query,
path
) do
relationship_path = Enum.reverse(Enum.map([relationship | path], &Map.get(&1, :name)))
request_path = [
:load,
relationship_path
]
dependencies =
case path do
[] ->
[request_path ++ [:authorization_filter]]
dependent_path ->
[
request_path ++ [:authorization_filter],
[:load, Enum.reverse(Enum.map(dependent_path, &Map.get(&1, :name))), :data],
[:load, Enum.reverse(Enum.map(dependent_path, &Map.get(&1, :name))), :query]
]
end
dependencies = [[:data, :data] | dependencies]
dependencies =
if relationship.type == :many_to_many &&
!lateral_join?(related_query, relationship) do
join_relationship = join_relationship(relationship)
join_relationship_path =
Enum.map(join_relationship_path(path, join_relationship), & &1.name)
[
[
:load,
join_relationship_path,
:data
]
| dependencies
]
else
dependencies
end
source =
[relationship | path]
|> Enum.reverse()
|> Enum.map_join(".", &Map.get(&1, :name))
Engine.Request.new(
action: Ash.Resource.Info.primary_action(relationship.destination, :read),
resource: relationship.destination,
name: "load #{source}",
api: related_query.api,
path: request_path,
query:
load_query(
relationship,
related_query,
path,
root_query
),
data:
Request.resolve(dependencies, fn data ->
base_query =
case get_in(data, 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, [
: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
with {:ok, new_query} <-
true_load_query(
relationship,
base_query,
data,
path
),
{:ok, results} <-
run_actual_query(
new_query,
base_query,
data,
path,
relationship,
source_query
) do
{:ok, results}
else
:nothing ->
{:ok, []}
{:error, error} ->
{:error, error}
end
end)
)
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,
related_query,
root_query,
path
) do
join_relationship = join_relationship(relationship)
join_relationship_name = join_relationship.name
case path do
[%{name: ^join_relationship_name} | _] ->
nil
path ->
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
lateral_join? = lateral_join?(related_query, relationship)
dependencies =
if lateral_join? do
dependencies ++
[
[
:load,
Enum.reverse(Enum.map(path, &Map.get(&1, :name))) ++ [relationship.name],
:data
]
]
else
dependencies
end
dependencies = [[:data, :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
Request.new(
action: Ash.Resource.Info.primary_action(relationship.destination, :read),
resource: relationship.through,
name: "load join #{join_relationship.name}",
api: related_query.api,
path: [:load, join_relationship_path_names],
query:
load_query(
join_relationship,
related_query,
Enum.reverse(join_relationship_path),
root_query
),
data:
Request.resolve(dependencies, fn
data ->
base_query =
case get_in(data, [
:load,
join_relationship_path_names,
:authorization_filter
]) do
nil ->
related_query
authorization_filter ->
Ash.Query.filter(related_query, ^authorization_filter)
end
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,
[: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
with {:ok, new_query} <-
true_load_query(
join_relationship,
base_query,
data,
path
),
new_query <-
add_join_destination_filter(
new_query,
lateral_join?,
data,
relationship,
Enum.reverse(Enum.map(path, &Map.get(&1, :name))) ++ [relationship.name]
),
{:ok, results} <-
run_actual_query(
new_query,
base_query,
data,
path,
join_relationship,
source_query
) do
{:ok, results}
else
:nothing ->
{:ok, []}
{:error, error} ->
{:error, error}
end
end)
)
end
end
defp add_join_destination_filter(query, true, data, relationship, destination_path) do
ids =
data
|> get_in([
:load,
destination_path,
:data
])
|> 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_field)
end)
filter = [{relationship.destination_field_on_join_table, [{:in, ids}]}]
Ash.Query.filter(query, ^filter)
end
defp add_join_destination_filter(query, false, _, _, _) do
query
end
defp lateral_join?(query, relationship) do
{offset, limit} = offset_and_limit(query)
resources =
[relationship.source, Map.get(relationship, :through), relationship.destination]
|> Enum.reject(&is_nil/1)
lateral_join =
(limit || offset) &&
Ash.DataLayer.data_layer_can?(
relationship.source,
{:lateral_join, resources}
)
!!lateral_join
end
defp run_actual_query(query, base_query, data, path, relationship, source_query) do
{offset, limit} = offset_and_limit(base_query)
source_data =
case path do
[] ->
get_in(data, [:data, :data])
path ->
data
|> Map.get(:load, %{})
|> Map.get(Enum.reverse(Enum.map(path, & &1.name)), %{})
|> Map.get(:data, [])
end
cond do
lateral_join?(query, relationship) && 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_field, relationship.source_field_on_join_table,
relationship},
{relationship.through, relationship.destination_field_on_join_table,
relationship.destination_field, join_relationship}
]
}
}
})
|> Ash.Query.set_context(relationship.context)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.sort(relationship.sort)
|> remove_relationships_from_load()
|> read(relationship.read_action)
lateral_join?(query, relationship) && (limit || offset) ->
query
|> Ash.Query.set_context(%{
data_layer: %{
lateral_join_source:
{source_data,
[
{source_query, relationship.source_field, relationship.destination_field,
relationship}
]}
}
})
|> Ash.Query.set_context(relationship.context)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.sort(relationship.sort)
|> remove_relationships_from_load()
|> read(relationship.read_action)
limit || offset ->
artificial_limit_and_offset(query, limit, offset, relationship)
true ->
query
|> Ash.Query.set_context(relationship.context)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.sort(relationship.sort)
|> remove_relationships_from_load()
|> read(relationship.read_action)
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) do
query
|> Ash.Query.set_context(relationship.context)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.sort(relationship.sort)
|> remove_relationships_from_load()
|> read(relationship.read_action)
|> case do
{:ok, results} ->
new_results =
results
|> Enum.group_by(fn record ->
Map.get(record, relationship.destination_field)
end)
|> Enum.flat_map(fn {_, group} ->
offset_records = Enum.drop(group, offset || 0)
if limit do
Enum.take(offset_records, limit)
else
offset_records
end
end)
{:ok, new_results}
{:error, error} ->
{:error, error}
end
end
defp read(query, action) do
action = action || primary_read(query)
query
|> load_for_calcs()
|> Ash.Actions.Read.unpaginated_read(action)
end
defp load_for_calcs(query) do
Enum.reduce(query.calculations || %{}, query, fn {_, calc}, query ->
calc.module.load(
query,
calc.opts,
Map.put(calc.context, :context, query.context)
)
end)
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 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 load_query_with_reverse_path(
root_query,
related_query,
reverse_path,
root_data_filter
) do
case Ash.Filter.parse(root_query.resource, root_query.filter, %{}, %{}) do
{:ok, nil} ->
related_query
|> Ash.Query.unset(:load)
|> Ash.Query.filter(
^put_nested_relationship(
[],
reverse_path,
root_data_filter,
false
)
)
|> Ash.Query.filter(^related_query.filter)
|> extract_errors()
{:ok, parsed} ->
filter =
put_nested_relationship(
[],
reverse_path,
root_data_filter,
false
)
related_query
|> Ash.Query.unset(:load)
|> Ash.Query.filter(^filter)
|> Ash.Query.filter(^put_nested_relationship([], reverse_path, parsed, false))
|> Ash.Query.filter(^related_query.filter)
|> extract_errors()
{:error, error} ->
{:error, error}
end
end
defp load_query(
relationship,
related_query,
path,
root_query
) do
Request.resolve([[:data, :data]], fn %{data: %{data: data}} ->
data =
case data do
%page{results: results} when page in [Ash.Page.Keyset, Ash.Page.Offset] ->
results
data ->
data
end
root_data_filter =
case data do
[] ->
false
[%resource{} | _] = items ->
pkey = Ash.Resource.Info.primary_key(resource)
[or: Enum.map(items, fn item -> item |> Map.take(pkey) |> Enum.to_list() end)]
end
path = Enum.reverse([relationship.name | Enum.map(path, & &1.name)])
case Ash.Resource.Info.reverse_relationship(
root_query.resource,
path
) do
nil ->
relationship.destination
|> Ash.Query.new(related_query.api)
|> Ash.Query.filter(^related_query.filter)
|> extract_errors()
reverse_path ->
load_query_with_reverse_path(
root_query,
related_query,
reverse_path,
root_data_filter
)
end
end)
end
defp extract_errors(%{errors: []} = item), do: {:ok, item}
defp extract_errors(%{errors: errors}), do: {:error, errors}
defp true_load_query(relationship, query, data, path) do
{source_field, path} =
if relationship.type == :many_to_many do
join_relationship = join_relationship(relationship)
{relationship.destination_field_on_join_table,
join_relationship_path(path, join_relationship) |> Enum.map(& &1.name)}
else
{relationship.source_field, path |> Enum.reverse() |> Enum.map(& &1.name)}
end
source_data =
case path do
[] ->
Map.get(data, :data)
path ->
data
|> Map.get(:load, %{})
|> Map.get(path, %{})
end
case source_data do
%{data: empty} when empty in [[], nil] ->
:nothing
empty when empty in [[], nil] ->
:nothing
_ ->
get_query(query, relationship, source_data, source_field)
end
end
defp get_query(query, relationship, source_data, source_field) do
{offset, limit} = offset_and_limit(query)
cond do
lateral_join?(query, relationship) ->
{:ok, Ash.Query.unset(query, :load)}
limit || offset ->
{:ok, Ash.Query.unset(query, [:load, :limit, :offset])}
true ->
related_data = Map.get(source_data || %{}, :data, [])
related_data =
case related_data do
%page{results: results} when page in [Ash.Page.Keyset, Ash.Page.Offset] ->
results
data ->
data
end
ids =
Enum.flat_map(related_data, fn data ->
data
|> Map.get(source_field)
|> List.wrap()
end)
filter_value =
case ids do
[id] ->
id
ids ->
[in: ids]
end
new_query =
query
|> Ash.Query.filter(^[{relationship.destination_field, filter_value}])
|> Ash.Query.unset(: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(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_field == destination_rel.destination_field &&
rel.destination_field == destination_rel.source_field &&
Map.fetch(rel, :source_field_on_join_table) ==
Map.fetch(destination_rel, :destination_field_on_join_table) &&
Map.fetch(rel, :destination_field_on_join_table) ==
Map.fetch(destination_rel, :source_field_on_join_table) &&
is_nil(destination_rel.context) &&
is_nil(rel.context)
end
defp put_nested_relationship(request_filter, path, value, records?) when not is_list(value) do
put_nested_relationship(request_filter, path, [value], records?)
end
defp put_nested_relationship(request_filter, [rel | rest], values, records?) do
[
{rel, put_nested_relationship(request_filter, rest, values, records?)}
]
end
defp put_nested_relationship(request_filter, [], [{field, value}], _) do
[{field, value} | request_filter]
end
defp put_nested_relationship(request_filter, [], [{field, _} | _] = keys, _) do
[{field, [{:in, Enum.map(keys, &elem(&1, 1))}]} | request_filter]
end
defp put_nested_relationship(request_filter, [], [values], _) do
List.wrap(request_filter) ++ List.wrap(values)
end
defp put_nested_relationship(request_filter, [], values, _) do
Keyword.update(request_filter, :or, values, &Kernel.++(&1, values))
end
end