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
Current section
Files
lib/ash/expr/expr.ex
# SPDX-FileCopyrightText: 2019 ash contributors <https://github.com/ash-project/ash/graphs/contributors>## SPDX-License-Identifier: MITdefmodule 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: valueend