Packages

ash

3.24.6
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 expr expr.ex
Raw

lib/ash/expr/expr.ex

# SPDX-FileCopyrightText: 2019 ash contributors <https://github.com/ash-project/ash/graphs/contributors>
#
# SPDX-License-Identifier: MIT
defmodule Ash.Expr do
@moduledoc "Tools to build Ash expressions"
alias Ash.Query.{BooleanExpression, Not}
@doc "Prepares a filter for comparison"
defdelegate to_sat_expression(resource, expression), to: Ash.Expr.SAT
@type t :: any
@pass_through_funcs [:where, :or_where, :expr, :@]
@aggregate_kinds Ash.Query.Aggregate.kinds()
@doc """
Evaluate an expression. See `eval/2` for more.
"""
def eval!(expression, opts \\ []) do
case eval(expression, opts) do
{:ok, result} ->
result
{:error, error} ->
raise Ash.Error.to_ash_error(error)
end
end
@doc """
Evaluate an expression. This function only works if you have no references, or if you provide the `record` option.
"""
def eval(expression, opts \\ []) do
context =
opts[:context]
|> Kernel.||(%{})
|> Map.put_new(:resource, opts[:resource])
|> Map.put(:eval?, true)
expression
|> Ash.Filter.hydrate_refs(context)
|> case do
{:ok, hydrated} ->
eval_hydrated(hydrated, opts)
{:error, error} ->
{:error, error}
end
end
@doc "Returns true if the value is or contains an expression"
@spec expr?(term) :: boolean()
def expr?({:_actor, _}), do: true
def expr?({:_arg, _}), do: true
def expr?({:_ref, _, _}), do: true
def expr?({:_combinations, _}), do: true
def expr?({:_parent, _, _}), do: true
def expr?({:_parent, _}), do: true
def expr?({:_atomic_ref, _}), do: true
def expr?({:_context, _}), do: true
def expr?(value)
when is_struct(value, Ash.Query.Not) or is_struct(value, Ash.Query.BooleanExpression) or
is_struct(value, Ash.Query.Call) or is_struct(value, Ash.Query.Ref) or
is_struct(value, Ash.Query.Exists) or
is_struct(value, Ash.Query.Parent) or
is_struct(value, Ash.Query.UpsertConflict) or
is_struct(value, Ash.CustomExpression) or
(is_struct(value) and is_map_key(value, :__predicate__?)) do
true
end
def expr?(value) when is_list(value) do
Enum.any?(value, &expr?/1)
end
def expr?(value) when is_map(value) and not is_struct(value) do
Enum.any?(value, fn {key, value} ->
expr?(key) || expr?(value)
end)
end
def expr?({left, right}) do
expr?(left) || expr?(right)
end
def expr?(tuple) when is_tuple(tuple) do
tuple |> Tuple.to_list() |> expr?()
end
def expr?(_), do: false
@doc "A template helper for using actor values in filter templates"
def actor(value), do: {:_actor, value}
@doc "A template helper for using the tenant in filter templates"
def tenant, do: :_tenant
@doc "A template helper for using action arguments in filter templates"
def arg(name), do: {:_arg, name}
@doc "A template helper for creating a reference"
def ref(name) when is_atom(name), do: {:_ref, [], name}
@doc "A template helper for creating a reference to a related path"
def ref(path, name) when is_list(path) and is_atom(name), do: {:_ref, path, name}
@doc "A template helper for creating a reference"
def combinations(name) when is_atom(name), do: {:_combinations, name}
@doc "A template helper for creating a parent reference"
def parent(expr), do: {:_parent, [], expr}
@doc "A template helper for referring to the most recent atomic expression applied to an update field"
def atomic_ref(expr), do: {:_atomic_ref, expr}
@doc """
A template helper for using query context in filter templates
An atom will get the value for a key, and a list will be accessed via `get_in`.
"""
def context(name), do: {:_context, name}
@doc false
def eval_hydrated(expression, opts \\ []) do
Ash.Filter.Runtime.load_and_eval(
opts[:record],
expression,
opts[:parent],
opts[:resource],
opts[:domain],
opts[:unknown_on_unknown_refs?],
opts[:actor],
opts[:tenant]
)
end
@spec where(Macro.t(), Macro.t()) :: t
defmacro where(left, right) do
quote do
Ash.Query.BooleanExpression.optimized_new(
:and,
Ash.Expr.expr(unquote(left)),
Ash.Expr.expr(unquote(right))
)
end
end
@spec or_where(Macro.t(), Macro.t()) :: t
defmacro or_where(left, right) do
quote do
Ash.Query.BooleanExpression.optimized_new(
:or,
Ash.Expr.expr(unquote(left)),
Ash.Expr.expr(unquote(right))
)
end
end
@doc """
Creates an expression calculation for use in sort and distinct statements.
## Examples
```elixir
Ash.Query.sort(query, [
{calc(string_upcase(name), :asc},
{calc(count_nils([field1, field2]), type: :integer), :desc})
])
```
"""
@spec calc(Macro.t(), opts :: Keyword.t()) :: t()
defmacro calc(expression, opts \\ []) do
quote generated: true do
require Ash.Expr
opts = unquote(opts)
type = opts[:type] && Ash.Type.get_type(opts[:type])
constraints = opts[:constraints] || []
name = opts[:name] || :__calc__
case Ash.Query.Calculation.new(
name,
Ash.Resource.Calculation.Expression,
[expr: Ash.Expr.expr(unquote(expression))],
type,
constraints
) do
{:ok, calc} -> calc
{:error, term} -> raise Ash.Error.to_ash_error(term)
end
end
end
@doc """
Creates an expression. See the [Expressions guide](/documentation/topics/reference/expressions.md) for more.
"""
@spec expr(Macro.t()) :: t()
defmacro expr(do: body) do
quote location: :keep do
Ash.Expr.expr(unquote(body))
end
end
defmacro expr(body) do
expr = do_expr(body)
quote location: :keep do
unquote(expr)
end
end
@doc false
def fill_template(
template,
opts
)
when is_list(opts) do
walk_template(template, fn
{:_actor, :_primary_key} ->
actor = opts[:actor]
if actor do
Map.take(actor, Ash.Resource.Info.primary_key(actor.__struct__))
end
{:_actor, field} when is_atom(field) or is_binary(field) ->
Map.get(opts[:actor] || %{}, field)
|> raise_if_not_loaded!(opts[:actor], List.wrap(field))
{:_actor, path} when is_list(path) ->
get_path(opts[:actor] || %{}, path)
|> raise_if_not_loaded!(opts[:actor], path)
:_tenant ->
opts[:tenant]
{:_arg, field} ->
args = opts[:args]
case Map.fetch(args, field) do
:error ->
Map.get(args, to_string(field))
{:ok, value} ->
value
end
{:_atomic_ref, field} when is_atom(field) ->
changeset = opts[:changeset]
if changeset do
Ash.Changeset.atomic_ref(changeset, field)
else
{:_atomic_ref, field}
end
{:_context, fields} when is_list(fields) ->
get_path(opts[:context], fields)
{:_context, field} ->
Map.get(opts[:context], field)
{:_ref, path, name} ->
%Ash.Query.Ref{
attribute: fill_template(name, Keyword.take(opts, [:actor, :tenant, :args, :context])),
relationship_path:
fill_template(path, Keyword.take(opts, [:actor, :tenant, :args, :context]))
}
{:_combinations, name} ->
%Ash.Query.Ref{
attribute: fill_template(name, Keyword.take(opts, [:actor, :tenant, :args, :context])),
combinations?: true
}
other ->
other
end)
end
@doc false
def fill_template(
template,
actor \\ nil,
args \\ %{},
context \\ %{},
changeset \\ nil
) do
fill_template(template,
actor: actor,
args: args,
context: context,
changeset: changeset
)
end
@doc false
def get_path(%Ash.NotLoaded{} = not_loaded, _), do: not_loaded
def get_path(map, [key]) when is_map(map) do
Map.get(map, key)
end
def get_path(map, [key | rest]) when is_map(map) do
get_path(get_path(map, [key]), rest)
end
def get_path(_, _), do: nil
defp raise_if_not_loaded!(%Ash.NotLoaded{}, actor, path) do
raise ArgumentError, """
Actor field is not loaded: #{inspect(path)}
Actor: #{inspect(actor)}
Ensure the field is loaded on the actor before using it in a filter template.
"""
end
defp raise_if_not_loaded!(value, _actor, _path), do: value
@doc false
def template_references_actor?(template) do
template_references?(template, fn
{:_actor, _} -> true
_ -> false
end)
end
def template_references_argument?(template) do
template_references?(template, fn
{:_arg, _} -> true
_ -> false
end)
end
def template_references_context?(template) do
template_references?(template, fn
{:_context, _} -> true
_ -> false
end)
end
def can_return_nil?(nil), do: true
def can_return_nil?(%Ash.Query.BooleanExpression{left: left, right: right}) do
can_return_nil?(left) || can_return_nil?(right)
end
def can_return_nil?(%Ash.Query.Not{expression: expression}) do
can_return_nil?(expression)
end
def can_return_nil?(%Ash.Query.Parent{expr: expr}) do
can_return_nil?(expr)
end
def can_return_nil?(%Ash.Query.UpsertConflict{}), do: true
def can_return_nil?(%Ash.Query.Exists{}), do: false
def can_return_nil?(%mod{__predicate__?: _} = pred) do
mod.can_return_nil?(pred)
end
def can_return_nil?(%Ash.Query.Ref{attribute: %{allow_nil?: false}}), do: false
def can_return_nil?(value) do
if Ash.Expr.expr?(value) do
true
else
false
end
end
@doc "Whether or not a given template contains an actor reference"
def template_references?(%{__struct__: Ash.Filter, expression: expression}, pred) do
template_references?(expression, pred)
end
def template_references?(%BooleanExpression{op: :and, left: left, right: right}, pred) do
template_references?(left, pred) || template_references?(right, pred)
end
def template_references?(%Not{expression: expression}, pred) do
template_references?(expression, pred)
end
def template_references?(%Ash.Query.Exists{expr: expr}, pred) do
template_references?(expr, pred)
end
def template_references?(%Ash.Query.Parent{expr: expr}, pred) do
template_references?(expr, pred)
end
def template_references?(
%Ash.CustomExpression{expression: expression, simple_expression: simple_expression},
pred
) do
template_references?(expression, pred) || template_references?(simple_expression, pred)
end
def template_references?(%{left: left, right: right}, pred) do
template_references?(left, pred) || template_references?(right, pred)
end
def template_references?(%{arguments: args}, pred) do
Enum.any?(args, &template_references?(&1, pred))
end
def template_references?(%Ash.Query.Call{args: args}, pred) do
Enum.any?(args, &template_references?(&1, pred))
end
def template_references?(list, pred) when is_list(list) do
Enum.any?(list, &template_references?(&1, pred))
end
def template_references?(map, pred) when is_map(map) and not is_struct(map) do
Enum.any?(map, &template_references?(&1, pred))
end
def template_references?(tuple, pred) when is_tuple(tuple) do
pred.(tuple) ||
tuple
|> Tuple.to_list()
|> Enum.any?(&template_references?(&1, pred))
end
def template_references?(thing, pred), do: pred.(thing)
@doc false
def walk_template(filter, mapper) when is_list(filter) do
case mapper.(filter) do
^filter ->
Enum.map(filter, &walk_template(&1, mapper))
other ->
walk_template(other, mapper)
end
end
def walk_template(%BooleanExpression{left: left, right: right} = expr, mapper) do
case mapper.(expr) do
^expr ->
%{
expr
| left: walk_template(left, mapper),
right: walk_template(right, mapper)
}
other ->
walk_template(other, mapper)
end
end
def walk_template(%Not{expression: expression} = not_expr, mapper) do
case mapper.(not_expr) do
^not_expr ->
%{not_expr | expression: walk_template(expression, mapper)}
other ->
walk_template(other, mapper)
end
end
def walk_template(%{__struct__: Ash.Filter, expression: expression} = filter, mapper) do
%{filter | expression: walk_template(expression, mapper)}
end
def walk_template(%Ash.Query.Parent{expr: expr} = this_expr, mapper) do
case mapper.(this_expr) do
^this_expr ->
%{this_expr | expr: walk_template(expr, mapper)}
other ->
walk_template(other, mapper)
end
end
def walk_template(%Ash.Query.Exists{expr: expr} = exists_expr, mapper) do
case mapper.(exists_expr) do
^exists_expr ->
%{exists_expr | expr: walk_template(expr, mapper)}
other ->
walk_template(other, mapper)
end
end
def walk_template(%{__predicate__?: _, left: left, right: right} = pred, mapper) do
case mapper.(pred) do
^pred ->
%{
pred
| left: walk_template(left, mapper),
right: walk_template(right, mapper)
}
other ->
walk_template(other, mapper)
end
end
def walk_template(%{__predicate__?: _, arguments: arguments} = func, mapper) do
case mapper.(func) do
^func ->
%{
func
| arguments: Enum.map(arguments, &walk_template(&1, mapper))
}
other ->
walk_template(other, mapper)
end
end
def walk_template(%Ash.Query.Call{args: args} = call, mapper) do
case mapper.(call) do
^call ->
%{
call
| args: Enum.map(args, &walk_template(&1, mapper))
}
other ->
walk_template(other, mapper)
end
end
def walk_template(%Ash.Query.Calculation{opts: opts} = calc, mapper) do
case mapper.(calc) do
^calc ->
new_opts =
Keyword.update(opts, :expr, nil, fn expr ->
walk_template(expr, mapper)
end)
%{calc | opts: new_opts}
other ->
walk_template(other, mapper)
end
end
def walk_template(filter, mapper) when is_map(filter) do
if Map.has_key?(filter, :__struct__) do
filter
else
case mapper.(filter) do
^filter ->
Enum.into(filter, %{}, &walk_template(&1, mapper))
other ->
walk_template(other, mapper)
end
end
end
def walk_template(tuple, mapper) when is_tuple(tuple) do
case mapper.(tuple) do
^tuple ->
tuple
|> Tuple.to_list()
|> Enum.map(&walk_template(&1, mapper))
|> List.to_tuple()
other ->
walk_template(other, mapper)
end
end
def walk_template(value, mapper), do: mapper.(value)
@operator_symbols Ash.Query.Operator.operator_symbols() -- [:is_nil]
@doc false
def do_expr(expr, escape? \\ true)
def do_expr({:|>, _, [first, {func, meta, args}]}, escape?) do
do_expr({func, meta, [first | args]}, escape?)
end
def do_expr({func, _, _} = expr, _escape?) when func in @pass_through_funcs do
expr
end
def do_expr({{:., _, [_, func]}, _, _} = expr, _escape?)
when func in @pass_through_funcs do
expr
end
def do_expr({op, _, nil}, escape?) when is_atom(op) do
soft_escape(%Ash.Query.Ref{relationship_path: [], attribute: op}, escape?)
end
def do_expr({op, _, Elixir}, escape?) when is_atom(op) do
soft_escape(%Ash.Query.Ref{relationship_path: [], attribute: op}, escape?)
end
def do_expr({:__aliases__, _, _} = expr, _escape?) do
expr
end
def do_expr({:^, _, [value]}, _escape?) do
value
end
def do_expr({{:., _, [Access, :get]}, _, [left, right]}, escape?) do
left = do_expr(left, false)
right = do_expr(right, false)
soft_escape(
quote do
[unquote(left), unquote(right)]
|> Ash.Query.Function.GetPath.new()
|> case do
{:ok, call} ->
call
{:error, error} ->
raise error
end
end,
escape?
)
end
def do_expr({{:., _, [_, _]} = left, _, []}, escape?) do
do_expr(left, escape?)
end
def do_expr(value, escape?) when is_list(value) do
Enum.map(value, &do_expr(&1, escape?))
end
def do_expr({:%, _, [struct_alias, {:%{}, _, fields}]}, escape?) do
struct_module = do_expr(struct_alias, false)
fields =
Enum.map(fields, fn {key, value} -> {do_expr(key, escape?), do_expr(value, escape?)} end)
{:%{}, [], [{:__struct__, struct_module} | fields]}
end
def do_expr({:%{}, _, keys}, escape?) do
{:%{}, [],
Enum.map(keys, fn {key, value} -> {do_expr(key, escape?), do_expr(value, escape?)} end)}
end
def do_expr({:{}, _, vals}, escape?) do
{%{}, [], Enum.map(vals, fn value -> do_expr(value, escape?) end)}
end
def do_expr({{:., _, [at_path, :exists]}, _, [path, expr]}, escape?) do
expr_with_at_path(path, at_path, expr, Ash.Query.Exists, escape?)
end
def do_expr({{:., _, [at_path, :exists]}, _, [path]}, escape?) do
expr_with_at_path(path, at_path, true, Ash.Query.Exists, escape?)
end
def do_expr({{:., _, [_, _]} = left, _, args}, escape?) do
args = Enum.map(args, &do_expr(&1, false))
case do_expr(left, escape?) do
{:%{}, [], parts} = other when is_list(parts) ->
if Enum.any?(parts, &(&1 == {:__struct__, Ash.Query.Ref})) do
ref = Map.new(parts)
soft_escape(
%Ash.Query.Call{
name: ref.attribute,
relationship_path: ref.relationship_path,
args: args,
operator?: false
},
escape?
)
else
other
end
%Ash.Query.Ref{} = ref ->
soft_escape(
%Ash.Query.Call{
name: ref.attribute,
relationship_path: ref.relationship_path,
args: args,
operator?: false
},
escape?
)
other ->
other
end
end
def do_expr({:ref, _, [field, path]} = expr, _escape?) do
raise ArgumentError, """
Usage of `ref/1` and `ref/2` must now be pinned, got: #{Macro.to_string(expr)}.
For example: `^ref(#{Macro.to_string(remove_pin(field))}, #{Macro.to_string(remove_pin(path))})`
"""
end
def do_expr({:ref, _, [field]} = expr, _escape?) do
raise ArgumentError, """
Usage of `ref/1` and `ref/2` must now be pinned, got: #{Macro.to_string(expr)}.
For example: `^ref(#{Macro.to_string(remove_pin(field))})`
"""
end
def do_expr(
{:<<>>, meta,
[
{:"::", _meta1,
[{{:., _meta2, [Kernel, :to_string]}, _meta3, [left]}, {:binary, _, _}]}
]},
escape?
) do
do_expr({:type, meta, [left, :string]}, escape?)
end
def do_expr(
{:<<>>, meta, [second_to_last, last]},
escape?
) do
do_expr({:<>, meta, [second_to_last, last]}, escape?)
end
def do_expr({:<<>>, _meta, [single]}, _escape?) do
single
end
def do_expr(
{:<<>>, meta, [next | rest]},
escape?
) do
do_expr({:<>, meta, [next, {:<<>>, meta, rest}]}, escape?)
end
def do_expr(
{:"::", meta, [{{:., _meta1, [Kernel, :to_string]}, _meta2, [left]}, {:binary, _, _}]},
escape?
) do
do_expr({:type, meta, [left, :string]}, escape?)
end
def do_expr({:., _, [left, right]} = ref, escape?) when is_atom(right) do
case do_ref(left, right) do
%Ash.Query.Ref{} = ref ->
soft_escape(ref, escape?)
:error ->
raise "Invalid reference! #{Macro.to_string(ref)}"
end
end
def do_expr({op, _, args}, escape?) when op in [:and, :or] do
args = Enum.map(args, &do_expr(&1, false))
soft_escape(BooleanExpression.optimized_new(op, Enum.at(args, 0), Enum.at(args, 1)), escape?)
end
def do_expr({op, _, [_, _] = args}, escape?)
when is_atom(op) and op in @operator_symbols do
args = Enum.map(args, &do_expr(&1, false))
if op in [:==, :!=, :>, :<, :>=, :<=] do
soft_escape(
quote do
args = unquote(args)
call = %Ash.Query.Call{name: unquote(op), args: args, operator?: true}
if Enum.any?(args, &is_nil/1) do
IO.warn(
"Comparing values with `nil` will always return `false`. Use `is_nil/1` instead. In: `#{inspect(call)}`"
)
end
call
end,
escape?
)
else
soft_escape(%Ash.Query.Call{name: op, args: args, operator?: true}, escape?)
end
end
def do_expr({parent, _, [expr]}, escape?) when parent in [:parent, :source, :parent_expr] do
expr = do_expr(expr, escape?)
soft_escape(
quote do
Ash.Query.Parent.new(unquote(expr))
end,
escape?
)
end
def do_expr({:upsert_conflict, _, [expr]}, escape?) do
expr = do_expr(expr, escape?)
soft_escape(
quote do
Ash.Query.UpsertConflict.new(unquote(expr))
end,
escape?
)
end
def do_expr({:exists, _, [{:__aliases__, _, _parts} = alias_ast, original_expr]}, escape?) do
processed_expr = do_expr(original_expr, false)
soft_escape(
quote do
%Ash.Query.Exists{
path: [],
resource: Macro.escape(unquote(alias_ast)),
expr: unquote(processed_expr),
at_path: [],
related?: false
}
end,
escape?
)
end
def do_expr({:exists, _, [{:__aliases__, _, _parts} = alias_ast]}, escape?) do
soft_escape(
quote do
%Ash.Query.Exists{
path: [],
resource: Macro.escape(unquote(alias_ast)),
expr: true,
at_path: [],
related?: false
}
end,
escape?
)
end
def do_expr({:exists, _, [module_atom, original_expr]}, escape?) when is_atom(module_atom) do
module_string = Atom.to_string(module_atom)
if String.match?(module_string, ~r/^[A-Z].*/) do
processed_expr = do_expr(original_expr, false)
soft_escape(
quote do
%Ash.Query.Exists{
path: [],
resource: unquote(module_atom),
expr: unquote(processed_expr),
at_path: [],
related?: false
}
end,
escape?
)
else
expr_with_at_path(module_atom, [], original_expr, Ash.Query.Exists, escape?)
end
end
def do_expr({:exists, _, [module_atom]}, escape?) when is_atom(module_atom) do
module_string = Atom.to_string(module_atom)
if String.match?(module_string, ~r/^[A-Z].*/) do
soft_escape(
%Ash.Query.Exists{
path: [],
resource: module_atom,
expr: true,
at_path: [],
related?: false
},
escape?
)
else
expr_with_at_path(module_atom, [], true, Ash.Query.Exists, escape?)
end
end
def do_expr({:exists, _, [path, original_expr]}, escape?) do
expr_with_at_path(path, [], original_expr, Ash.Query.Exists, escape?)
end
def do_expr({:exists, _, [path]}, escape?) do
expr_with_at_path(path, [], true, Ash.Query.Exists, escape?)
end
def do_expr({left, _, [{op, _, [right]}]}, escape?)
when is_atom(op) and op in @operator_symbols and is_atom(left) and left != :not do
do_expr({op, [], [left, right]}, escape?)
end
def do_expr({:not, _, [expression]}, escape?) do
expression = do_expr(expression, false)
soft_escape(Not.new(expression), escape?)
end
def do_expr({:cond, _, [[do: options]]}, escape?) do
options
|> Enum.map(fn {:->, _, [condition, result]} ->
{condition, result}
end)
|> cond_to_if_tree()
|> do_expr(escape?)
end
def do_expr({:case, _, _}, _escape?) do
raise ArgumentError,
message: """
`case` expressions are not supported in Ash expressions.
Please use `cond` expressions instead. For example:
# Instead of:
case role do
:principal -> 1
:teacher -> 2
:student -> 3
end
# Use:
cond do
role == :principal -> 1
role == :teacher -> 2
role == :student -> 3
end
"""
end
def do_expr({:lazy, _, args}, escape?) do
soft_escape(%Ash.Query.Call{name: :lazy, args: args, operator?: false}, escape?)
end
def do_expr({:sigil_i, _, [{:<<>>, _, [str]}, mods]}, escape?) do
soft_escape(Ash.CiString.sigil_i(str, mods), escape?)
end
def do_expr({:fragment, _, [{_, _, [{:<<>>, _, [query]}, []]} = first | args]}, escape?)
when is_binary(query) do
args = Enum.map(args, &do_expr(&1, false))
soft_escape(%Ash.Query.Call{name: :fragment, args: [first | args], operator?: false}, escape?)
end
def do_expr({:fragment, _, [first | _] = args}, escape?)
when is_binary(first) or is_function(first) do
last_arg = List.last(args)
args =
if Keyword.keyword?(last_arg) && Keyword.has_key?(last_arg, :do) do
Enum.map(:lists.droplast(args), &do_expr(&1, false)) ++
[
Enum.map(last_arg, fn {key, arg_value} ->
{key, do_expr(arg_value, false)}
end)
]
else
Enum.map(args, &do_expr(&1, false))
end
soft_escape(%Ash.Query.Call{name: :fragment, args: args, operator?: false}, escape?)
end
def do_expr(
{:&, _,
[
{:/, _,
[
{{:., _, [{:__aliases__, _, [_]}, _]}, _, []},
_
]}
]} = expr,
_
) do
expr
end
def do_expr(
{:&, _,
[
{:/, _,
[
{{:., _, [v, _]}, _, []},
_
]}
]} = expr,
_
)
when is_atom(v) do
expr
end
def do_expr(
{:&, _,
[
{:/, _,
[
{{:., _, [{mod, _, context}, _]}, _, []},
_
]}
]} = expr,
_
)
when is_atom(mod) and is_atom(context) do
expr
end
def do_expr(
{:&, _, _} = expr,
_
) do
raise """
The only kind of anonymous functions allowed in expressions are in the format `&Module.function/arity`.
Got: #{Macro.to_string(expr)}
"""
end
def do_expr(
{:fn, _, _} = expr,
_
) do
raise """
The only kind of anonymous functions allowed in expressions are in the format `&Module.function/arity`.
Got: #{Macro.to_string(expr)}
"""
end
def do_expr({:fragment, _, [{:&, _, _} | _] = args}, escape?) do
last_arg = List.last(args)
args =
if Keyword.keyword?(last_arg) && Keyword.has_key?(last_arg, :do) do
Enum.map(:lists.droplast(args), &do_expr(&1, false)) ++
[
Enum.map(last_arg, fn {key, arg_value} ->
{key, do_expr(arg_value, false)}
end)
]
else
Enum.map(args, &do_expr(&1, false))
end
soft_escape(%Ash.Query.Call{name: :fragment, args: args, operator?: false}, escape?)
end
def do_expr({:fragment, _, [{m, f, a} | _] = args}, escape?)
when is_atom(m) and is_atom(f) and is_list(a) do
last_arg = List.last(args)
args =
if Keyword.keyword?(last_arg) && Keyword.has_key?(last_arg, :do) do
Enum.map(:lists.droplast(args), &do_expr(&1, false)) ++
[
Enum.map(last_arg, fn {key, arg_value} ->
{key, do_expr(arg_value, false)}
end)
]
else
Enum.map(args, &do_expr(&1, false))
end
soft_escape(%Ash.Query.Call{name: :fragment, args: args, operator?: false}, escape?)
end
def do_expr({:fragment, _, [first | _]}, _escape?) do
raise """
To prevent SQL injection attacks, fragment(...) allows only two specific kinds of values
1. A string literal *not* interpolated. This is for use with data layers like `AshPostgres.
2. A one argument function or an MFA *not* interpolated. This is for use with data layers like `Ash.DataLayer.Simple` and `Ash.DataLayer.Ets`.
Got: #{Macro.to_string(first)}
"""
end
def do_expr({op, _, args}, escape?) when is_atom(op) and is_list(args) do
last_arg = List.last(args)
args =
if Keyword.keyword?(last_arg) && Keyword.has_key?(last_arg, :do) do
Enum.map(:lists.droplast(args), &do_expr(&1, false)) ++
[
Enum.map(last_arg, fn {key, arg_value} ->
{key, do_expr(arg_value, false)}
end)
]
else
Enum.map(args, &do_expr(&1, false))
end
soft_escape(%Ash.Query.Call{name: op, args: args, operator?: false}, escape?)
end
def do_expr({left, _, _}, escape?) when is_tuple(left), do: do_expr(left, escape?)
def do_expr({left, right}, escape?) do
left = do_expr(left, escape?)
right = do_expr(right, escape?)
soft_escape({left, right}, escape?)
end
def do_expr(other, _), do: other
@doc """
Determines the types for a given operator or function module and its arguments.
Given an operator or function module (e.g. `Ash.Query.Operator.In`), resolves the
concrete types of the arguments based on the module's declared type signatures and
any registered operator overloads. Returns `{resolved_types, return_type}` where
`resolved_types` is a list of `{type, constraints}` tuples (or `nil` for unresolvable
arguments) and `return_type` is the resolved return type.
This is the primary type resolution mechanism used by data layer expression compilers.
It performs actual type validation including coercion checks, unlike
`Ash.Type.determine_types/2` which only resolves vague types (`:same`/`:any`).
"""
def determine_types(mod, args, returns \\ nil, nested? \\ false)
def determine_types(Ash.Query.Function.Type, [_, type], _returns, _nested?) do
{type, []}
end
def determine_types(Ash.Query.Function.Type, [_, type, constraints], _returns, _nested?) do
{type, constraints}
end
def determine_types(mod, values, known_result, _nested?) do
Code.ensure_compiled(mod)
known_result =
case known_result do
nil -> nil
{:array, type} -> {{:array, type}, []}
{type, constraints} -> {type, constraints}
type -> {type, []}
end
name =
cond do
function_exported?(mod, :operator, 0) ->
mod.operator()
function_exported?(mod, :name, 0) ->
mod.name()
true ->
nil
end
{types, returns} =
cond do
:erlang.function_exported(mod, :types, 0) ->
{mod.types(), mod.returns()}
:erlang.function_exported(mod, :args, 0) ->
{mod.args(), mod.returns()}
true ->
{[:any], [:any]}
end
overloads = Ash.Query.Operator.operator_overloads(name) || %{}
overload_index_cap = Enum.count(overloads) - 1
if types == :var_args || returns == :no_return || returns == :unknown do
[]
else
{more_match_types, overload_cast_as_types, overload_returns} =
overloads
|> Enum.reduce({[], [], []}, fn {match_types, value}, {match_acc, cast_acc, return_acc} ->
case value do
{cast_as_types, return_type} when is_list(cast_as_types) ->
{[match_types | match_acc], [cast_as_types | cast_acc], [return_type | return_acc]}
return_type ->
{[match_types | match_acc], [nil | cast_acc], [return_type | return_acc]}
end
end)
|> then(fn {m, c, r} -> {Enum.reverse(m), Enum.reverse(c), Enum.reverse(r)} end)
# Put overloads first so they have priority over built-in types like :same
types = Enum.concat(more_match_types, types)
cast_as_types_list =
Enum.concat(
overload_cast_as_types,
Stream.duplicate(nil, length(types))
)
returns = Enum.concat(overload_returns, returns)
returns =
Enum.map(returns, fn
{:array, any} when any in [:same, :any] -> {:array, any}
any when any in [:same, :any] -> any
{type, constraints} -> get_type({type, constraints})
type -> get_type({type, []})
end)
normalize_types = fn types ->
Enum.map(types, fn
{:array, any} when any in [:same, :any] -> {:array, any}
any when any in [:same, :any] -> any
{type, constraints} -> get_type({type, constraints})
type -> get_type({type, []})
end)
end
types =
Enum.map(types, fn
types when is_list(types) -> normalize_types.(types)
types -> types
end)
cast_as_types_list =
Enum.map(cast_as_types_list, fn
types when is_list(types) -> normalize_types.(types)
types -> types
end)
types
|> Enum.zip(cast_as_types_list)
|> Enum.zip(returns)
|> Enum.map(fn {{match_types, cast_as_types}, returns} ->
{match_types, cast_as_types, returns}
end)
end
|> Enum.reject(fn {match_types, _, _} -> match_types == :any end)
|> Enum.filter(fn {match_types, _, _} ->
match_types == :same ||
length(match_types) == length(values)
end)
|> Enum.map(fn {match_types, cast_as_types, returns} ->
basis =
cond do
!returns ->
nil
returns == :same ->
known_result
returns == {:array, :same} ->
case known_result do
{:array, type} ->
case type do
{type, constraints} ->
{type, constraints}
type ->
{type, []}
end
_ ->
nil
end
true ->
nil
end
types_and_values =
if match_types == :same do
Enum.map(values, &{:same, &1})
else
Enum.zip(match_types, values)
end
types_and_values
|> Enum.with_index()
|> Enum.reduce_while(
%{
must_adopt_basis: [],
basis: basis,
types: [],
fallback_basis: nil,
last_resort?: false
},
fn
{{vague_type, value}, index}, acc when vague_type in [:any, :same] ->
case determine_type(value) do
{:ok, {type, constraints}} ->
case acc[:basis] do
nil ->
if vague_type == :any do
acc = Map.update!(acc, :types, &[{type, constraints} | &1])
{:cont, Map.put(acc, :basis, {type, constraints})}
else
acc =
acc
|> Map.update!(:types, &[nil | &1])
|> Map.put(:fallback_basis, {type, constraints})
{:cont, Map.update!(acc, :must_adopt_basis, &[{index, fn x -> x end} | &1])}
end
{^type, matched_constraints} ->
{:cont, Map.update!(acc, :types, &[{type, matched_constraints} | &1])}
_basis ->
{:halt, :error}
end
:error ->
acc = Map.update!(acc, :types, &[nil | &1])
{:cont, Map.update!(acc, :must_adopt_basis, &[{index, fn x -> x end} | &1])}
end
{{{:array, vague_type}, value}, index}, acc when vague_type in [:any, :same] ->
case determine_type(value) do
{:ok, {{:array, type}, constraints}} ->
case acc[:basis] do
nil ->
if vague_type == :any do
acc = Map.update!(acc, :types, &[{{:array, type}, items: constraints} | &1])
{:cont, Map.put(acc, :basis, {type, constraints})}
else
acc =
acc
|> Map.update!(:types, &[nil | &1])
|> Map.put(:fallback_basis, {type, constraints})
{:cont,
Map.update!(
acc,
:must_adopt_basis,
&[
{index,
fn {type, constraints} -> {{:array, type}, items: constraints} end}
| &1
]
)}
end
{^type, matched_constraints} ->
{:cont,
Map.update!(
acc,
:types,
&[{{:array, type}, items: matched_constraints} | &1]
)}
_ ->
{:halt, :error}
end
_ ->
acc = Map.update!(acc, :types, &[nil | &1])
{:cont,
Map.update!(
acc,
:must_adopt_basis,
&[
{index, fn {type, constraints} -> {{:array, type}, items: constraints} end}
| &1
]
)}
end
{{{type, constraints}, value}, index}, acc ->
determined_type = determine_type(value)
cond do
!Ash.Expr.expr?(value) && !matches_type?(type, value, constraints) ->
case Ash.Type.coerce(type, value, constraints) do
{:ok, _} ->
{:cont, Map.update!(acc, :types, &[{type, constraints} | &1])}
_ ->
{:halt, :error}
end
match?({:ok, {determined_type, _}} when determined_type != type, determined_type) ->
{:halt, :error}
match?({:ok, _}, determined_type) ->
{:cont, Map.update!(acc, :types, &[elem(determined_type, 1) | &1])}
Ash.Expr.expr?(value) ->
if index < overload_index_cap do
{:cont,
acc |> Map.update!(:types, &[{type, []} | &1]) |> Map.put(:last_resort?, true)}
else
{:cont, Map.update!(acc, :types, &[{type, []} | &1])}
end
true ->
{:cont, Map.update!(acc, :types, &[{type, constraints} | &1])}
end
{{type, value}, index}, acc ->
determined_type = determine_type(value)
cond do
!Ash.Expr.expr?(value) && !matches_type?(type, value, []) ->
case Ash.Type.coerce(type, value, []) do
{:ok, _} ->
{:cont, Map.update!(acc, :types, &[{type, []} | &1])}
_ ->
{:halt, :error}
end
match?({:ok, {determined_type, _}} when determined_type != type, determined_type) ->
{:halt, :error}
match?({:ok, _}, determined_type) ->
{:cont, Map.update!(acc, :types, &[elem(determined_type, 1) | &1])}
Ash.Expr.expr?(value) ->
if index < overload_index_cap do
{:cont,
acc |> Map.update!(:types, &[{type, []} | &1]) |> Map.put(:last_resort?, true)}
else
{:cont, Map.update!(acc, :types, &[{type, []} | &1])}
end
true ->
{:cont, Map.update!(acc, :types, &[{type, []} | &1])}
end
end
)
|> then(fn
%{basis: nil, fallback_basis: fallback_basis} = data when not is_nil(fallback_basis) ->
%{data | basis: fallback_basis}
data ->
data
end)
|> case do
:error ->
nil
%{basis: nil, must_adopt_basis: [], types: types, last_resort?: last_resort?} ->
if returns not in [:same, :any, {:array, :same}, {:array, :any}] do
output_types =
cast_as_types || Enum.reverse(types)
# must_adopt_basis is empty means all types matched exactly
{output_types, returns, 0, last_resort?}
end
%{basis: nil, must_adopt_basis: _} ->
nil
%{
basis: basis,
must_adopt_basis: basis_adopters,
types: types,
last_resort?: last_resort?
} ->
returns =
case returns do
same when same in [:same, :any] ->
basis
same when same in [{:array, :same}, {:array, :any}] ->
{type, constraints} = basis
{{:array, type}, items: constraints}
other ->
other
end
output_types =
cast_as_types ||
basis_adopters
|> Enum.reduce(
Enum.reverse(types),
fn {index, function_of_basis}, types ->
List.replace_at(types, index, function_of_basis.(basis))
end
)
{output_types, returns, Enum.count(basis_adopters), last_resort?}
end
end)
|> Enum.filter(& &1)
|> case do
[{types, returns, _, _}] ->
{types, returns}
types ->
types =
Enum.flat_map(types, fn {types, returns, basis_adopters, last_resort?} ->
if last_resort? do
[]
else
[{types, returns, basis_adopters}]
end
end)
select_matches(types, length(values), values)
end
end
defp select_matches([], value_count, _values) do
{Enum.map(1..value_count, fn _ -> nil end), nil}
end
defp select_matches(results, value_count, values) do
case Enum.find(results, fn
{_type, _returns, 0} ->
true
_ ->
false
end) do
{type, returns, 0} ->
{type, returns}
_ ->
results
|> Enum.map(fn {types, {type, constraints}, _} ->
types =
Enum.map(types, fn {type, constraints} ->
get_type({type, constraints})
end)
{types, get_type({type, constraints})}
end)
|> Enum.reject(fn {types, _} ->
types
|> Enum.zip(values)
|> Enum.any?(fn {{type, constraints}, value} ->
!Ash.Expr.expr?(value) and
!(matches_type?(type, value, constraints) ||
match?({:ok, _}, Ash.Type.coerce(type, value, constraints)))
end)
end)
|> case do
[] ->
{Enum.map(1..value_count, fn _ -> nil end), nil}
results ->
arg_types =
1..value_count
|> Enum.map(fn i ->
possible_types =
Enum.map(results, fn {types, _} ->
Enum.at(types, i - 1)
end)
case Enum.find(possible_types, fn {type, constraints} ->
matches_type?(type, Enum.at(values, i - 1), constraints)
end) do
type when not is_nil(type) ->
type
nil ->
case Enum.uniq_by(possible_types, &elem(&1, 0)) do
[single] ->
Enum.find(possible_types, single, fn {_, constraints} ->
constraints != []
end)
_ ->
nil
end
end
end)
all_returns = Enum.map(results, &elem(&1, 1))
case Enum.find_value(results, fn {types, returns} ->
if types == arg_types do
returns
end
end) do
nil ->
case Enum.uniq(all_returns) do
[single] ->
{arg_types, single}
_ ->
case Enum.uniq_by(all_returns, &elem(&1, 0)) do
[single] ->
{arg_types,
Enum.find(all_returns, single, fn {_, constraints} ->
constraints != []
end)}
_ ->
{arg_types, nil}
end
end
returns ->
{arg_types, returns}
end
end
end
end
def determine_type(value) do
case value do
%{__struct__: Ash.Query.Function.Type, arguments: [_, type, constraints]} ->
if res = get_type({type, constraints}) do
{:ok, res}
else
:error
end
%{__struct__: Ash.Query.Function.Type, arguments: [_, type]} ->
if res = get_type({type, []}) do
{:ok, res}
else
:error
end
%{__struct__: Ash.Query.Ref, attribute: %{type: type, constraints: constraints}} ->
if Ash.Type.ash_type?(type) do
if res = get_type({type, constraints}) do
{:ok, res}
else
:error
end
else
:error
end
%{__struct__: Ash.Query.Ref, attribute: %{type: type}} ->
if Ash.Type.ash_type?(type) do
if res = get_type({type, []}) do
{:ok, res}
else
:error
end
else
:error
end
%{__predicate__?: true} ->
{:ok, {Ash.Type.Boolean, []}}
%{__struct__: Ash.Query.BooleanExpression} ->
{:ok, {Ash.Type.Boolean, []}}
%{__struct__: Ash.Query.Exists} ->
{:ok, {Ash.Type.Boolean, []}}
%{__struct__: Ash.Query.Aggregate, type: type, constraints: constraints}
when not is_nil(type) ->
if Ash.Type.ash_type?(type) do
if res = get_type({type, constraints || []}) do
{:ok, res}
else
:error
end
else
:error
end
%{__struct__: Ash.Query.Aggregate, kind: kind, type: nil} ->
determine_aggregate_type_from_kind(kind, :any, [])
%{__struct__: Ash.Query.Parent, expr: expr} ->
determine_type(expr)
%{__struct__: Ash.Query.UpsertConflict, expr: expr} ->
determine_type(expr)
%{__struct__: Ash.Query.Function.GetPath, arguments: [left, path]} ->
determine_get_path_type(left, path)
%mod{__predicate__?: _, arguments: arguments} ->
case determine_types(mod, arguments, nil, true) do
{_, nil} -> :error
{_, type} -> {:ok, type}
end
%mod{__predicate__?: _, left: left, right: right} ->
case determine_types(mod, [left, right], nil, true) do
{_, nil} -> :error
{_, type} -> {:ok, type}
end
value when is_map(value) and not is_struct(value) and value != %{} ->
determine_map_type(value)
%{__struct__: Ash.Query.Call, name: name, args: args}
when name in @aggregate_kinds ->
determine_inline_aggregate_type(name, args)
%{__struct__: struct_module} when is_atom(struct_module) ->
determine_struct_type(struct_module)
_ ->
:error
end
end
defp determine_struct_type(struct_module) do
if Ash.Type.ash_type?(struct_module) do
{:ok, {struct_module, []}}
else
:error
end
end
defp determine_map_type(map) do
if Enum.all?(map, fn {key, _} -> is_atom(key) end) do
Enum.reduce_while(map, {:ok, []}, fn {key, val_expr}, {:ok, acc} ->
case determine_type(val_expr) do
{:ok, {type, constraints}} ->
allow_nil? = can_return_nil?(val_expr)
{:cont,
{:ok, [{key, [type: type, constraints: constraints, allow_nil?: allow_nil?]} | acc]}}
:error ->
{:halt, :error}
end
end)
|> case do
{:ok, fields} ->
# Maps aren't ordered — sort by the atom's string name so the
# output is stable across compiles for codegen consumers.
stable_fields = Enum.sort_by(fields, fn {key, _} -> Atom.to_string(key) end)
{:ok, {Ash.Type.Map, [fields: stable_fields]}}
:error ->
:error
end
else
:error
end
end
defp determine_inline_aggregate_type(name, args) do
{field_type, field_constraints} = extract_aggregate_field_info(args)
determine_aggregate_type_from_kind(name, field_type, field_constraints)
end
defp extract_aggregate_field_info(args) do
with opts when is_list(opts) <- Enum.at(args, 1),
true <- Keyword.keyword?(opts),
%{type: type, constraints: constraints} when not is_nil(type) <-
Keyword.get(opts, :field) do
{type, constraints || []}
else
_ -> {:any, []}
end
end
defp determine_aggregate_type_from_kind(kind, field_type, field_constraints) do
case Ash.Query.Aggregate.kind_to_type(kind, field_type, field_constraints) do
{:ok, type, constraints} ->
if res = get_type({type, constraints}) do
{:ok, res}
else
:error
end
{:error, _} ->
:error
end
end
defp determine_get_path_type(left, path) do
path = List.wrap(path)
with {:ok, {type, constraints}} <- determine_type(left),
{:ok, {type, constraints}} <- walk_get_path({type, constraints || []}, path) do
{:ok, {type, constraints}}
else
_ -> :error
end
end
defp walk_get_path({type, constraints}, []) do
{:ok, {type, constraints}}
end
defp walk_get_path({{:array, type}, constraints}, [segment | rest]) when is_integer(segment) do
walk_get_path({type, get_item_constraints(constraints)}, rest)
end
defp walk_get_path({type, constraints}, [segment | rest]) when is_integer(segment) do
case Ash.Type.get_type(type) do
{:array, inner_type} ->
walk_get_path({inner_type, get_item_constraints(constraints)}, rest)
_ ->
:error
end
end
defp walk_get_path({type, constraints}, [segment | rest])
when is_atom(segment) or is_binary(segment) do
constraints = constraints || []
cond do
type && Ash.Type.embedded_type?(type) ->
base_type =
if Ash.Type.NewType.new_type?(type) do
Ash.Type.NewType.subtype_of(type)
else
type
end
case Ash.Resource.Info.attribute(base_type, segment) do
%{type: attr_type, constraints: attr_constraints} ->
walk_get_path({attr_type, attr_constraints}, rest)
_ ->
:error
end
type && Ash.Type.composite?(type, constraints) ->
case find_composite_member(type, constraints, segment) do
{:ok, {member_type, member_constraints}} ->
walk_get_path({member_type, member_constraints || []}, rest)
:error ->
:error
end
true ->
:error
end
end
defp walk_get_path(_type, _path), do: :error
defp find_composite_member(type, constraints, key) do
type
|> Ash.Type.composite_types(constraints || [])
|> Enum.map(fn
{name, member_type, member_constraints} ->
{name, nil, member_type, member_constraints}
{name, storage_key, member_type, member_constraints} ->
{name, storage_key, member_type, member_constraints}
end)
|> Enum.find(fn {name, storage_key, _member_type, _member_constraints} ->
matches_key?(name, key) || matches_key?(storage_key, key)
end)
|> case do
{_, _, member_type, member_constraints} ->
{:ok, {member_type, member_constraints}}
_ ->
:error
end
end
defp matches_key?(nil, _key), do: false
defp matches_key?(key_value, key) when is_binary(key) do
to_string(key_value) == key
end
defp matches_key?(key_value, key) when is_atom(key) do
key_value == key || to_string(key_value) == Atom.to_string(key)
end
defp get_item_constraints(constraints) when is_list(constraints) do
Keyword.get(constraints, :items) || []
end
defp get_item_constraints(_constraints), do: []
defp get_type({type, constraints}) do
if type = Ash.Type.get_type(type) do
{type, constraints}
end
end
defp matches_type?(type, value, constraints) do
type = Ash.Type.get_type(type)
Ash.Type.ash_type?(type) && Ash.Type.matches_type?(type, value, constraints)
end
defp expr_with_at_path(path, at_path, expr, struct, escape?) do
expr = do_expr(expr, escape?)
path =
case path do
{:^, _, [value]} ->
value
{:., _, [left, right]} ->
ref = do_ref(left, right)
ref.relationship_path ++ [ref.attribute]
{{:., _, [left, right]}, _, _} ->
ref = do_ref(left, right)
ref.relationship_path ++ [ref.attribute]
{atom, _, _} when is_atom(atom) ->
[atom]
atom when is_atom(atom) ->
[atom]
path when is_list(path) ->
path
other ->
raise "Invalid value used in the first argument in exists, i.e exists(#{Macro.to_string(other)}, #{Macro.to_string(expr)})"
end
at_path =
case at_path do
{:^, _, [value]} ->
value
{:., _, [left, right]} ->
ref = do_ref(left, right)
ref.relationship_path ++ [ref.attribute]
{{:., _, [left, right]}, _, _} ->
ref = do_ref(left, right)
ref.relationship_path ++ [ref.attribute]
{atom, _, _} when is_atom(atom) ->
[atom]
path when is_list(path) ->
path
other ->
raise "Invalid value used in the first argument in exists, i.e exists(#{Macro.to_string(other)}, #{Macro.to_string(at_path)})"
end
soft_escape(
quote do
unquote(struct).new(
unquote(path),
unquote(expr),
unquote(at_path)
)
end,
escape?
)
end
defp cond_to_if_tree([{condition, result}]) do
{:if, [], [cond_condition(condition), [do: result]]}
end
defp cond_to_if_tree([{condition, result} | rest]) do
{:if, [], [cond_condition(condition), [do: result, else: cond_to_if_tree(rest)]]}
end
defp cond_condition([condition]) do
condition
end
defp cond_condition([condition | rest]) do
{:and, [], [condition, cond_condition(rest)]}
end
defp soft_escape(%_{} = val, _) do
{:%{}, [], Map.to_list(val)}
end
defp soft_escape(other, _), do: other
defp do_ref({left, _, nil}, _right) when left in @operator_symbols do
raise ArgumentError, "invalid use of `.` in expression. Use `[]` to access nested fields"
end
defp do_ref({left, _, nil}, right) do
%Ash.Query.Ref{relationship_path: [left], attribute: right}
end
defp do_ref({{:., _, [_, _]} = left, _, _}, right) do
do_ref(left, right)
end
defp do_ref({:., _, [_left, _right]}, far_right) when far_right in @operator_symbols do
raise ArgumentError, "invalid use of `.` in expression. Use `[]` to access nested fields"
end
defp do_ref({:., _, [left, right]}, far_right) do
case do_ref(left, right) do
%Ash.Query.Ref{relationship_path: path, attribute: attribute} = ref ->
%{ref | relationship_path: path ++ [attribute], attribute: far_right}
:error ->
:error
end
end
defp do_ref({left, _, _}, right) when left in @operator_symbols and is_atom(right) do
raise ArgumentError, "invalid use of `.` in expression. Use `[]` to access nested fields"
end
defp do_ref({left, _, _}, right) when is_atom(left) and right in @operator_symbols do
raise ArgumentError, "invalid use of `.` in expression. Use `[]` to access nested fields"
end
defp do_ref({left, _, _}, right) when is_atom(left) and is_atom(right) do
%Ash.Query.Ref{relationship_path: [left], attribute: right}
end
defp do_ref(_left, _right) do
:error
end
defp remove_pin({:^, _, [value]}), do: value
defp remove_pin(value), do: value
end