Packages

ash

1.13.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 query query.ex
Raw

lib/ash/query/query.ex

defmodule Ash.Query do
@moduledoc """
Utilties around constructing/manipulating ash queries.
Ash queries are used for read actions and side loads, and ultimately
map to queries to a resource's data layer.
"""
defstruct [
:api,
:resource,
:filter,
:data_layer_query,
aggregates: %{},
side_load: [],
calculations: %{},
context: %{},
sort: [],
limit: nil,
offset: 0,
errors: [],
valid?: true
]
@type t :: %__MODULE__{}
defimpl Inspect do
import Inspect.Algebra
def inspect(query, opts) do
sort? = query.sort != []
side_load? = query.side_load != []
aggregates? = query.aggregates != %{}
calculations? = query.calculations != %{}
limit? = not is_nil(query.limit)
offset? = not (is_nil(query.offset) || query.offset == 0)
filter? = not is_nil(query.filter)
errors? = not Enum.empty?(query.errors)
container_doc(
"#Ash.Query<",
[
concat("resource: ", inspect(query.resource)),
or_empty(concat("filter: ", to_doc(query.filter, opts)), filter?),
or_empty(concat("sort: ", to_doc(query.sort, opts)), sort?),
or_empty(concat("limit: ", to_doc(query.limit, opts)), limit?),
or_empty(concat("offset: ", to_doc(query.offset, opts)), offset?),
or_empty(concat("side_load: ", to_doc(query.side_load, opts)), side_load?),
or_empty(concat("aggregates: ", to_doc(query.aggregates, opts)), aggregates?),
or_empty(concat("calculations: ", to_doc(query.calculations, opts)), calculations?),
or_empty(concat("errors: ", to_doc(query.errors, opts)), errors?)
],
">",
opts,
fn str, _ -> str end
)
end
defp or_empty(value, true), do: value
defp or_empty(_, false), do: empty()
end
alias Ash.Actions.Sort
alias Ash.Error.Query.{AggregatesNotSupported, InvalidLimit, InvalidOffset}
alias Ash.Error.SideLoad.{InvalidQuery, NoSuchRelationship}
alias Ash.Query.{Aggregate, Calculation}
@doc "Create a new query."
def new(resource, api \\ nil) when is_atom(resource) do
%__MODULE__{
api: api,
filter: nil,
resource: resource
}
|> set_data_layer_query()
end
@spec load(t(), atom | list(atom) | Keyword.t()) :: t()
def load(query, fields) when not is_list(fields) do
load(query, List.wrap(fields))
end
def load(query, fields) do
query = to_query(query)
Enum.reduce(fields, query, fn
{field, %__MODULE__{} = nested}, query ->
side_load(query, [{field, nested}])
{field, rest}, query ->
cond do
rel = Ash.Resource.relationship(query.resource, field) ->
nested_query = load(rel.destination, rest)
side_load(query, [{field, nested_query}])
calculation = Ash.Resource.calculation(query.resource, field) ->
{module, opts} = module_and_opts(calculation.calculation)
with {:ok, args} <- validate_arguments(calculation, rest),
{:ok, calculation} <-
Calculation.new(
calculation.name,
module,
opts,
args
) do
calculation = %{calculation | load: field}
%{query | calculations: Map.put(query.calculations, field, calculation)}
end
true ->
add_error(query, :load, "Invalid load #{inspect(field)}")
end
field, query ->
do_load(query, field)
end)
end
defp module_and_opts({module, opts}), do: {module, opts}
defp module_and_opts(module), do: {module, []}
defp do_load(query, field) do
cond do
Ash.Resource.attribute(query.resource, field) ->
query
Ash.Resource.relationship(query.resource, field) ->
side_load(query, field)
aggregate = Ash.Resource.aggregate(query.resource, field) ->
related = Ash.Resource.related(query.resource, aggregate.relationship_path)
with %{valid?: true} = aggregate_query <-
build(related, filter: aggregate.filter),
{:ok, query_aggregate} <-
Aggregate.new(
query.resource,
aggregate.name,
aggregate.kind,
aggregate.relationship_path,
aggregate_query
) do
query_aggregate = %{query_aggregate | load: field}
new_aggregates = Map.put(query.aggregates, aggregate.name, query_aggregate)
%{query | aggregates: new_aggregates}
else
%{errors: errors} ->
add_error(query, :aggregates, Ash.Error.to_ash_error(errors))
{:error, error} ->
add_error(query, :aggregates, Ash.Error.to_ash_error(error))
end
calculation = Ash.Resource.calculation(query.resource, field) ->
{module, opts} =
case calculation.calculation do
{module, opts} -> {module, opts}
module -> {module, []}
end
with {:ok, args} <- validate_arguments(calculation, %{}),
{:ok, calculation} <-
Calculation.new(calculation.name, module, opts, args) do
calculation = %{calculation | load: field}
%{query | calculations: Map.put(query.calculations, field, calculation)}
else
{:error, error} ->
add_error(query, :load, error)
end
true ->
add_error(query, :load, "Could not load #{inspect(field)}")
end
end
defp validate_arguments(calculation, args) do
Enum.reduce_while(calculation.arguments, {:ok, %{}}, fn argument, {:ok, arg_values} ->
value = default(Map.get(args, argument.name), argument.default)
if is_nil(value) do
if argument.allow_nil? do
{:cont, {:ok, Map.put(arg_values, argument.name, nil)}}
else
{:halt, {:error, "Argument #{argument.name} is required"}}
end
else
with {:ok, casted} <- Ash.Type.cast_input(argument.type, value),
:ok <-
Ash.Type.apply_constraints(argument.type, casted, argument.constraints) do
{:cont, {:ok, Map.put(arg_values, argument.name, casted)}}
else
{:error, error} ->
{:halt, {:error, error}}
end
end
end)
end
defp default(nil, {module, function, args}), do: apply(module, function, args)
defp default(nil, value) when is_function(value, 0), do: value.()
defp default(nil, value), do: value
defp default(value, _), do: value
@spec put_context(t(), atom, term) :: t()
def put_context(query, key, value) do
%{query | context: Map.put(query.context, key, value)}
end
@spec set_context(t(), map) :: t()
def set_context(query, map) do
%{
query
| context:
Map.merge(query.context, map, fn _k, v1, v2 ->
if is_map(v1) and is_map(v2) do
Map.merge(v1, v2)
else
v2
end
end)
}
end
def unload(query, fields) do
query = to_query(query)
Enum.reduce(fields, query, fn field, query ->
case field do
{field, rest} ->
new_side_loads = do_unload_side_load(query.side_load, {field, rest})
%{query | side_load: new_side_loads}
field ->
do_unload(query, field)
end
end)
end
defp do_unload(query, field) do
cond do
Ash.Resource.attribute(query.resource, field) ->
query
Ash.Resource.relationship(query.resource, field) ->
%{query | side_load: Keyword.delete(query.side_load, field)}
Ash.Resource.aggregate(query.resource, field) ->
new_aggregates =
Enum.reduce(query.aggregates, %{}, fn
{_field, %{load: ^field}}, acc ->
acc
{field, aggregate}, acc ->
Map.put(acc, field, aggregate)
end)
%{query | aggregates: new_aggregates}
end
end
defp do_unload_side_load(%__MODULE__{} = query, unload) do
%{query | side_load: do_unload_side_load(query.side_load, unload)}
end
defp do_unload_side_load(side_loads, {field, rest}) do
Enum.reduce(side_loads, [], fn
^field, acc ->
acc
{^field, value}, acc ->
new_value =
rest
|> List.wrap()
|> Enum.reduce(value, &do_unload_side_load(&2, &1))
[{field, new_value} | acc]
value, acc ->
[value | acc]
end)
|> Enum.reverse()
end
defp do_unload_side_load(side_loads, field) do
do_unload_side_load(side_loads, {field, []})
end
@spec build(Ash.resource(), Ash.api() | nil, Keyword.t()) :: t()
def build(resource, api \\ nil, keyword) do
Enum.reduce(keyword, new(resource, api), fn
{:filter, value}, query ->
filter(query, value)
{:sort, value}, query ->
sort(query, value)
{:limit, value}, query ->
limit(query, value)
{:offset, value}, query ->
offset(query, value)
{:side_load, value}, query ->
side_load(query, value)
{:aggregate, {name, type, relationship}}, query ->
aggregate(query, name, type, relationship)
{:aggregate, {name, type, relationship, agg_query}}, query ->
aggregate(query, name, type, relationship, agg_query)
{:calculate, {name, module_and_opts}}, query ->
calculate(query, name, module_and_opts)
{:calculate, {name, module_and_opts, context}}, query ->
calculate(query, name, module_and_opts, context)
end)
end
@doc "Set the query's api, and any side loaded query's api"
def set_api(query, api) do
query = to_query(query)
%{query | api: api, side_load: set_side_load_api(query.side_load, api)}
end
@doc """
Adds an aggregation to the query.
Aggregations are made available on the `aggregates` field of the records returned
The only aggregate available currently is a `count` aggregate. They filter option accepts
either a filter or a keyword list of options to supply to build a limiting query for that aggregate.
However, currently only filters are accepted.
"""
@spec aggregate(
t() | Ash.resource(),
atom(),
Ash.aggregate_kind(),
atom | list(atom),
Ash.query() | nil
) :: t()
def aggregate(query, name, type, relationship, agg_query \\ nil) do
query = to_query(query)
relationship = List.wrap(relationship)
if Ash.Resource.data_layer_can?(query.resource, {:aggregate, type}) do
agg_query =
case agg_query do
nil ->
nil
%__MODULE__{} = agg_query ->
agg_query
options when is_list(options) ->
build(Ash.Resource.related(query.resource, relationship), options)
end
case Aggregate.new(query.resource, name, type, relationship, agg_query) do
{:ok, aggregate} ->
new_aggregates = Map.put(query.aggregates, aggregate.name, aggregate)
set_data_layer_query(%{query | aggregates: new_aggregates})
{:error, error} ->
add_error(query, :aggregate, error)
end
else
add_error(
query,
:aggregate,
AggregatesNotSupported.exception(resource: query.resource, feature: "using")
)
end
end
@doc """
Adds a calculation to the query.
Calculations are made available on the `calculations` field of the records returned
The `module_and_opts` argument accepts either a `module` or a `{module, opts}`. For more information
on what that module should look like, see `Ash.Calculation`.
More features for calculations, like passing anonymous functions, will be supported in the future.
"""
def calculate(query, name, module_and_opts, context \\ %{}) do
query = to_query(query)
{module, opts} =
case module_and_opts do
{module, opts} -> {module, opts}
module -> {module, []}
end
case Calculation.new(name, module, opts, context) do
{:ok, calculation} ->
%{query | calculations: Map.put(query.calculations, name, calculation)}
{:error, error} ->
add_error(query, :calculations, error)
end
end
@doc "Limit the results returned from the query"
@spec limit(t() | Ash.resource(), nil | integer()) :: t()
def limit(query, nil), do: to_query(query)
def limit(query, limit) when is_integer(limit) do
query = to_query(query)
if Ash.Resource.data_layer_can?(query.resource, :limit) do
query
|> Map.put(:limit, max(0, limit))
|> set_data_layer_query()
else
add_error(query, :limit, "Data layer does not support limits")
end
end
def limit(query, limit) do
add_error(query, :offset, InvalidLimit.exception(limit: limit))
end
@doc "Skip the first n records"
@spec offset(t() | Ash.resource(), nil | integer()) :: t()
def offset(query, nil), do: to_query(query)
def offset(query, offset) when is_integer(offset) do
query = to_query(query)
if Ash.Resource.data_layer_can?(query.resource, :offset) do
query
|> Map.put(:offset, max(0, offset))
|> set_data_layer_query()
else
add_error(query, :offset, "Data layer does not support offset")
end
end
def offset(query, offset) do
query
|> to_query()
|> add_error(:offset, InvalidOffset.exception(offset: offset))
end
defp side_load(query, statement) do
query = to_query(query)
with sanitized_statement <- List.wrap(sanitize_side_loads(statement)),
:ok <- validate_side_load(query.resource, sanitized_statement),
new_side_loads <- merge_side_load(query.side_load, sanitized_statement) do
%{query | side_load: new_side_loads}
else
{:error, errors} ->
Enum.reduce(errors, query, &add_error(&2, :side_load, &1))
end
end
def validate_side_load(resource, side_loads, path \\ []) do
case do_validate_side_load(resource, side_loads, path) do
[] -> :ok
errors -> {:error, errors}
end
end
def do_validate_side_load(_resource, %Ash.Query{} = query, path) do
if query.limit || (query.offset && query.offset != 0) do
[{:error, InvalidQuery.exception(query: query, side_load_path: Enum.reverse(path))}]
else
case query.errors do
[] ->
[]
_errors ->
[
{:error,
InvalidQuery.exception(
query: query,
side_load_path: Enum.reverse(path)
)}
]
end
end
end
def do_validate_side_load(resource, {atom, _} = tuple, path) when is_atom(atom) do
do_validate_side_load(resource, [tuple], path)
end
def do_validate_side_load(resource, side_loads, path) when is_list(side_loads) do
side_loads
|> List.wrap()
|> Enum.flat_map(fn
{_key, %Ash.Query{}} ->
[]
{key, value} ->
case Ash.Resource.relationship(resource, key) do
nil ->
[
{:error,
NoSuchRelationship.exception(
resource: resource,
relationship: key,
side_load_path: Enum.reverse(path)
)}
]
relationship ->
validate_matching_query_and_continue(value, resource, key, path, relationship)
end
end)
end
@spec filter(t() | Ash.resource(), nil | false | Ash.filter() | Keyword.t()) :: t()
def filter(query, nil), do: to_query(query)
def filter(query, %Ash.Filter{} = filter) do
query = to_query(query)
if Ash.Resource.data_layer_can?(query.resource, :filter) do
new_filter =
case query.filter do
nil ->
{:ok, filter}
existing_filter ->
Ash.Filter.add_to_filter(existing_filter, filter, :and, query.aggregates)
end
case new_filter do
{:ok, filter} ->
set_data_layer_query(%{query | filter: filter})
{:error, error} ->
add_error(query, :filter, error)
end
else
add_error(query, :filter, "Data layer does not support filtering")
end
end
def filter(query, statement) do
query = to_query(query)
if Ash.Resource.data_layer_can?(query.resource, :filter) do
filter =
if query.filter do
Ash.Filter.add_to_filter(query.filter, statement, :and, query.aggregates)
else
Ash.Filter.parse(query.resource, statement, query.aggregates)
end
case filter do
{:ok, filter} ->
query
|> Map.put(:filter, filter)
|> set_data_layer_query()
{:error, error} ->
add_error(query, :filter, error)
end
else
add_error(query, :filter, "Data layer does not support filtering")
end
end
@spec sort(t() | Ash.resource(), Ash.sort()) :: t()
def sort(query, sorts) do
query = to_query(query)
if Ash.Resource.data_layer_can?(query.resource, :sort) do
sorts
|> List.wrap()
|> Enum.reduce(query, fn
{sort, direction}, query ->
%{query | sort: query.sort ++ [{sort, direction}]}
sort, query ->
%{query | sort: query.sort ++ [{sort, :asc}]}
end)
|> validate_sort()
|> set_data_layer_query()
else
add_error(query, :sort, "Data layer does not support sorting")
end
end
@spec unset(Ash.resource() | t(), atom | [atom]) :: t()
def unset(query, keys) when is_list(keys) do
query = to_query(query)
keys
|> Enum.reduce(query, fn key, query ->
if key in [:api, :resource] do
query
else
struct(query, [{key, Map.get(%__MODULE__{}, key)}])
end
end)
|> set_data_layer_query()
end
def unset(query, key) do
if key in [:api, :resource] do
to_query(query)
else
query
|> to_query()
|> struct([{key, Map.get(%__MODULE__{}, key)}])
|> set_data_layer_query()
end
end
@doc false
def data_layer_query(%{resource: resource} = ash_query, opts \\ []) do
if Ash.Resource.data_layer_can?(resource, :read) do
query = Ash.DataLayer.resource_to_query(resource)
filter_aggregates =
if ash_query.filter do
Ash.Filter.used_aggregates(ash_query.filter)
else
[]
end
sort_aggregates =
Enum.flat_map(ash_query.sort, fn {field, _} ->
case Map.fetch(ash_query.aggregates, field) do
:error ->
[]
{:ok, agg} ->
[agg]
end
end)
aggregates = Enum.uniq_by(filter_aggregates ++ sort_aggregates, & &1.name)
with {:ok, query} <-
add_aggregates(query, ash_query.resource, aggregates),
{:ok, query} <-
Ash.DataLayer.sort(query, ash_query.sort, resource),
{:ok, query} <- maybe_filter(query, ash_query, opts),
{:ok, query} <-
Ash.DataLayer.limit(query, ash_query.limit, resource),
{:ok, query} <-
Ash.DataLayer.offset(query, ash_query.offset, resource) do
{:ok, Ash.DataLayer.set_context(resource, query, ash_query.context)}
else
{:error, error} -> {:error, error}
end
else
{:error, "Resource does not support reading"}
end
end
defp add_aggregates(query, resource, aggregates) do
Enum.reduce_while(aggregates, {:ok, query}, fn aggregate, {:ok, query} ->
case Ash.DataLayer.add_aggregate(query, aggregate, resource) do
{:ok, query} -> {:cont, {:ok, query}}
{:error, error} -> {:halt, {:error, error}}
end
end)
end
defp validate_sort(%{resource: resource, sort: sort} = query) do
case Sort.process(resource, sort, query.aggregates) do
{:ok, new_sort} -> %{query | sort: new_sort}
{:error, error} -> add_error(query, :sort, error)
end
end
defp add_error(query, key, message) do
query = to_query(query)
message =
if is_binary(message) do
"#{key}: #{message}"
else
message
end
%{
query
| errors: [Map.put(Ash.Error.to_ash_error(message), :path, key) | query.errors],
valid?: false
}
end
defp set_data_layer_query(query) do
case data_layer_query(query) do
{:ok, data_layer_query} -> %{query | data_layer_query: data_layer_query}
{:error, error} -> add_error(query, :data_layer_query, error)
end
end
defp validate_matching_query_and_continue(value, resource, key, path, relationship) do
%{destination: relationship_resource} = relationship
case value do
%__MODULE__{resource: query_resource} = destination_query
when query_resource != relationship_resource ->
[
InvalidQuery.exception(
resource: resource,
relationship: key,
query: destination_query,
side_load_path: Enum.reverse(path)
)
]
other ->
do_validate_side_load(relationship.destination, other, [key | path])
end
end
defp maybe_filter(query, %{filter: nil}, _) do
{:ok, query}
end
defp maybe_filter(query, ash_query, opts) do
case Ash.DataLayer.filter(query, ash_query.filter, ash_query.resource) do
{:ok, filtered} ->
if Keyword.get(opts, :only_validate_filter?, true) do
{:ok, query}
else
{:ok, filtered}
end
{:error, error} ->
{:error, error}
end
end
defp set_side_load_api(nil, _), do: nil
defp set_side_load_api([], _), do: []
defp set_side_load_api(%__MODULE__{} = query, api) do
set_api(query, api)
end
defp set_side_load_api(side_loads, api) do
Enum.map(side_loads, fn {key, further} ->
{key, set_side_load_api(further, api)}
end)
end
defp to_query(%__MODULE__{} = query), do: query
defp to_query(resource) do
resource
|> new()
|> Ash.DataLayer.transform_query()
end
defp merge_side_load([], right), do: sanitize_side_loads(right)
defp merge_side_load(left, []), do: sanitize_side_loads(left)
defp merge_side_load(
%__MODULE__{side_load: left_side_loads},
%__MODULE__{side_load: right_side_loads} = query
) do
%{query | side_load: merge_side_load(left_side_loads, right_side_loads)}
end
defp merge_side_load(%__MODULE__{} = query, right) when is_list(right) do
side_load(query, right)
end
defp merge_side_load(left, %Ash.Query{} = query) when is_list(left) do
side_load(query, left)
end
defp merge_side_load(left, right) when is_atom(left), do: merge_side_load([{left, []}], right)
defp merge_side_load(left, right) when is_atom(right), do: merge_side_load(left, [{right, []}])
defp merge_side_load(left, right) when is_list(left) and is_list(right) do
right
|> sanitize_side_loads()
|> Enum.reduce(sanitize_side_loads(left), fn {rel, rest}, acc ->
Keyword.update(acc, rel, rest, &merge_side_load(&1, rest))
end)
end
defp sanitize_side_loads(side_load) when is_atom(side_load), do: {side_load, []}
defp sanitize_side_loads(%Ash.Query{} = query) do
Map.update!(query, :side_load, &sanitize_side_loads/1)
end
defp sanitize_side_loads(side_loads) do
side_loads
|> List.wrap()
|> Enum.map(fn
{key, value} ->
{key, sanitize_side_loads(value)}
side_load_part ->
cond do
is_atom(side_load_part) -> {side_load_part, []}
is_list(side_load_part) -> sanitize_side_loads(side_load_part)
true -> side_load_part
end
end)
end
end