From 3db94e935bdcc91954835b054bc2a10ff5241ac9 Mon Sep 17 00:00:00 2001 From: Jinkyou Son Date: Fri, 3 Apr 2026 14:28:20 +0900 Subject: [PATCH 1/6] add Ash.Expr.expression() type and rename broad type references from t() --- lib/ash/changeset/changeset.ex | 8 +++---- lib/ash/custom_expression.ex | 6 ++--- lib/ash/data_layer/data_layer.ex | 10 ++++---- lib/ash/expr/expr.ex | 1 + lib/ash/expr/sat.ex | 4 ++-- lib/ash/filter/filter.ex | 2 +- lib/ash/policy/check.ex | 6 ++--- lib/ash/policy/filter_check.ex | 4 ++-- lib/ash/query/combination.ex | 2 +- lib/ash/reactor/dsl/bulk_update.ex | 2 +- lib/ash/resource/actions/create.ex | 2 +- lib/ash/resource/builder.ex | 2 +- lib/ash/resource/change/builtins.ex | 6 ++--- lib/ash/resource/change/change.ex | 36 ++++++++++++++--------------- lib/ash/resource/identity.ex | 2 +- lib/ash/resource/validation.ex | 12 +++++----- lib/ash/type/type.ex | 20 ++++++++-------- 17 files changed, 63 insertions(+), 62 deletions(-) diff --git a/lib/ash/changeset/changeset.ex b/lib/ash/changeset/changeset.ex index b4f0816450..5c230d5964 100644 --- a/lib/ash/changeset/changeset.ex +++ b/lib/ash/changeset/changeset.ex @@ -1676,7 +1676,7 @@ defmodule Ash.Changeset do # ugly because it can end up reading like "not is not equal to" but # ultimately produces the correct results. @spec atomic_condition([{module(), keyword()}], Ash.Changeset.t(), map() | struct()) :: - {:atomic, Ash.Expr.t() | boolean()} | {:not_atomic, String.t()} + {:atomic, Ash.Expr.expression() | boolean()} | {:not_atomic, String.t()} def atomic_condition(where, changeset, context) do # Handle both map and struct contexts (e.g., Ash.Resource.Change.Context) context_map = if is_struct(context), do: Map.from_struct(context), else: context @@ -2427,7 +2427,7 @@ defmodule Ash.Changeset do - `fully_atomic_changeset/4` for creating fully atomic changesets - `change_attribute/3` for regular (non-atomic) attribute changes """ - @spec atomic_update(t(), atom(), {:atomic, Ash.Expr.t()} | Ash.Expr.t()) :: t() + @spec atomic_update(t(), atom(), {:atomic, Ash.Expr.expression()} | Ash.Expr.expression()) :: t() def atomic_update(changeset, key, value, opts \\ []) def atomic_update(changeset, key, {:atomic, value}, _opts) do @@ -2543,7 +2543,7 @@ defmodule Ash.Changeset do end) end - @spec atomic_set(t(), atom(), {:atomic, Ash.Expr.t()} | Ash.Expr.t()) :: t() + @spec atomic_set(t(), atom(), {:atomic, Ash.Expr.expression()} | Ash.Expr.expression()) :: t() def atomic_set(changeset, key, value, opts \\ []) def atomic_set(%{action_type: :update} = changeset, key, value, opts) do @@ -7561,7 +7561,7 @@ defmodule Ash.Changeset do Used by optimistic locking. See `Ash.Resource.Change.Builtins.optimistic_lock/1` for more. """ - @spec filter(t(), Ash.Expr.t()) :: t() + @spec filter(t(), Ash.Expr.expression()) :: t() def filter(changeset, expr) when expr in [nil, %{}, []] do changeset end diff --git a/lib/ash/custom_expression.ex b/lib/ash/custom_expression.ex index 78a6e2ff34..0e08981ba2 100644 --- a/lib/ash/custom_expression.ex +++ b/lib/ash/custom_expression.ex @@ -70,12 +70,12 @@ defmodule Ash.CustomExpression do @callback expression( data_layer :: Ash.DataLayer.t(), - arguments :: list(Ash.Expr.t()) + arguments :: list(Ash.Expr.expression()) ) :: - {:ok, Ash.Expr.t()} | :unknown + {:ok, Ash.Expr.expression()} | :unknown @doc false - @spec expression(module(), module(), list()) :: {:ok, Ash.Expr.t()} | :unknown + @spec expression(module(), module(), list()) :: {:ok, Ash.Expr.expression()} | :unknown def expression(module, data_layer, arguments) do result = apply(module, :expression, [data_layer, arguments]) diff --git a/lib/ash/data_layer/data_layer.ex b/lib/ash/data_layer/data_layer.ex index 52c5764930..dec57188fd 100644 --- a/lib/ash/data_layer/data_layer.ex +++ b/lib/ash/data_layer/data_layer.ex @@ -193,7 +193,7 @@ defmodule Ash.DataLayer do upsert?: boolean, action_select: list(atom), upsert_keys: nil | list(atom), - upsert_condition: Ash.Expr.t() | nil, + upsert_condition: Ash.Expr.expression() | nil, return_skipped_upsert?: boolean, identity: Ash.Resource.Identity.t() | nil, select: list(atom), @@ -210,7 +210,7 @@ defmodule Ash.DataLayer do @type bulk_update_options :: %{ return_records?: boolean, action_select: list(atom), - calculations: list({Ash.Query.Calculation.t(), Ash.Expr.t()}), + calculations: list({Ash.Query.Calculation.t(), Ash.Expr.expression()}), select: list(atom), tenant: term() } @@ -308,7 +308,7 @@ defmodule Ash.DataLayer do @callback in_transaction?(Ash.Resource.t()) :: boolean @callback source(Ash.Resource.t()) :: String.t() @callback rollback(Ash.Resource.t(), term) :: no_return - @callback calculate(Ash.Resource.t(), list(Ash.Expr.t()), context :: map) :: + @callback calculate(Ash.Resource.t(), list(Ash.Expr.expression()), context :: map) :: {:ok, term} | {:error, term} @callback prefer_transaction?(Ash.Resource.t()) :: boolean @callback prefer_transaction_for_atomic_updates?(Ash.Resource.t()) :: boolean @@ -397,14 +397,14 @@ defmodule Ash.DataLayer do Ash.DataLayer.functions(resource) end - @spec calculate(Ash.Resource.t(), list(Ash.Expr.t()), context :: map) :: + @spec calculate(Ash.Resource.t(), list(Ash.Expr.expression()), context :: map) :: {:ok, list(term)} | {:error, Ash.Error.t()} def calculate(resource, exprs, context) do calculate(Ash.DataLayer.data_layer(resource), resource, exprs, context) end @doc false - @spec calculate(module(), Ash.Resource.t(), list(Ash.Expr.t()), map()) :: + @spec calculate(module(), Ash.Resource.t(), list(Ash.Expr.expression()), map()) :: {:ok, list(term)} | {:error, Ash.Error.t()} def calculate(data_layer_module, resource, exprs, context) do Ash.BehaviourHelpers.call_and_validate_return( diff --git a/lib/ash/expr/expr.ex b/lib/ash/expr/expr.ex index ee56007c1a..b705ee0212 100644 --- a/lib/ash/expr/expr.ex +++ b/lib/ash/expr/expr.ex @@ -10,6 +10,7 @@ defmodule Ash.Expr do defdelegate to_sat_expression(resource, expression), to: Ash.Expr.SAT @type t :: any + @type expression :: term() @pass_through_funcs [:where, :or_where, :expr, :@] @aggregate_kinds Ash.Query.Aggregate.kinds() diff --git a/lib/ash/expr/sat.ex b/lib/ash/expr/sat.ex index c90d8c8a1c..0cbc6f39d7 100644 --- a/lib/ash/expr/sat.ex +++ b/lib/ash/expr/sat.ex @@ -12,8 +12,8 @@ defmodule Ash.Expr.SAT do @dialyzer {:nowarn_function, overlap?: 2} @doc "Prepares a filter for comparison" - @spec to_sat_expression(Ash.Resource.t(), Ash.Expr.t()) :: - Crux.Expression.t(Ash.Expr.t()) + @spec to_sat_expression(Ash.Resource.t(), Ash.Expr.expression()) :: + Crux.Expression.t(Ash.Expr.expression()) def to_sat_expression(resource, expression) do expression |> consolidate_relationships(resource) diff --git a/lib/ash/filter/filter.ex b/lib/ash/filter/filter.ex index 24520ffa5b..0046eee003 100644 --- a/lib/ash/filter/filter.ex +++ b/lib/ash/filter/filter.ex @@ -660,7 +660,7 @@ defmodule Ash.Filter do Use this when your attribute is configured with `filterable? :simple_equality`, and you want to to find the value that it is being filtered on with (if any). """ - @spec fetch_simple_equality_predicate(Ash.Expr.t(), atom()) :: {:ok, term()} | :error + @spec fetch_simple_equality_predicate(Ash.Expr.expression(), atom()) :: {:ok, term()} | :error def fetch_simple_equality_predicate(expression, attribute) do expression |> find(&simple_eq?(&1, attribute), false) diff --git a/lib/ash/policy/check.ex b/lib/ash/policy/check.ex index 4ebdaac393..765332eb12 100644 --- a/lib/ash/policy/check.ex +++ b/lib/ash/policy/check.ex @@ -56,7 +56,7 @@ defmodule Ash.Policy.Check do Return a keyword list filter that will be applied to the query being made, and will scope the results to match the rule """ - @callback auto_filter(actor(), authorizer(), options()) :: Keyword.t() | Ash.Expr.t() + @callback auto_filter(actor(), authorizer(), options()) :: Keyword.t() | Ash.Expr.expression() @doc """ An optional callback, that allows the check to work with policies set to `access_type :runtime` @@ -167,7 +167,7 @@ defmodule Ash.Policy.Check do @doc false @spec auto_filter(module(), actor(), authorizer(), options()) :: - Keyword.t() | Ash.Expr.t() | nil + Keyword.t() | Ash.Expr.expression() | nil def auto_filter(module, actor, authorizer, opts) do result = apply(module, :auto_filter, [actor, authorizer, opts]) @@ -186,7 +186,7 @@ defmodule Ash.Policy.Check do @doc false @spec auto_filter_not(module(), actor(), authorizer(), options()) :: - Keyword.t() | Ash.Expr.t() | nil + Keyword.t() | Ash.Expr.expression() | nil def auto_filter_not(module, actor, authorizer, opts) do result = apply(module, :auto_filter_not, [actor, authorizer, opts]) diff --git a/lib/ash/policy/filter_check.ex b/lib/ash/policy/filter_check.ex index 0ae4a89d5a..2fe9563d10 100644 --- a/lib/ash/policy/filter_check.ex +++ b/lib/ash/policy/filter_check.ex @@ -40,8 +40,8 @@ defmodule Ash.Policy.FilterCheck do optional(any) => any } - @callback filter(actor :: term, context(), options()) :: Keyword.t() | Ash.Expr.t() - @callback reject(actor :: term, context(), options()) :: Keyword.t() | Ash.Expr.t() + @callback filter(actor :: term, context(), options()) :: Keyword.t() | Ash.Expr.expression() + @callback reject(actor :: term, context(), options()) :: Keyword.t() | Ash.Expr.expression() @optional_callbacks [reject: 3] defmacro __using__(_) do diff --git a/lib/ash/query/combination.ex b/lib/ash/query/combination.ex index 790013f6cb..43878a8537 100644 --- a/lib/ash/query/combination.ex +++ b/lib/ash/query/combination.ex @@ -8,7 +8,7 @@ defmodule Ash.Query.Combination do """ @type t :: %__MODULE__{ - filter: Ash.Expr.t(), + filter: Ash.Expr.expression(), sort: Ash.Sort.t(), limit: pos_integer() | nil, offset: pos_integer() | nil, diff --git a/lib/ash/reactor/dsl/bulk_update.ex b/lib/ash/reactor/dsl/bulk_update.ex index bd87bc7f51..f4e11e0497 100644 --- a/lib/ash/reactor/dsl/bulk_update.ex +++ b/lib/ash/reactor/dsl/bulk_update.ex @@ -73,7 +73,7 @@ defmodule Ash.Reactor.Dsl.BulkUpdate do allow_stream_with: :keyset | :offset | :full_read, assume_casted?: boolean, async?: boolean, - atomic_update: %{optional(atom) => Ash.Expr.t()}, + atomic_update: %{optional(atom) => Ash.Expr.expression()}, authorize_changeset_with: :filter | :error, authorize_query_with: :filter | :error, authorize_query?: boolean, diff --git a/lib/ash/resource/actions/create.ex b/lib/ash/resource/actions/create.ex index 9b1854c6e9..1347e09bf3 100644 --- a/lib/ash/resource/actions/create.ex +++ b/lib/ash/resource/actions/create.ex @@ -57,7 +57,7 @@ defmodule Ash.Resource.Actions.Create do | :replace_all | {:replace, list(atom)} | {:replace_all_except, list(atom)}, - upsert_condition: Ash.Expr.t() | nil, + upsert_condition: Ash.Expr.expression() | nil, touches_resources: list(atom), arguments: list(Ash.Resource.Actions.Argument.t()), primary?: boolean, diff --git a/lib/ash/resource/builder.ex b/lib/ash/resource/builder.ex index 1bda2d307f..9e08aa84b2 100644 --- a/lib/ash/resource/builder.ex +++ b/lib/ash/resource/builder.ex @@ -511,7 +511,7 @@ defmodule Ash.Resource.Builder do Spark.Dsl.Builder.input(), name :: atom, type :: Ash.Type.t(), - calculation :: module | {module, Keyword.t()} | Ash.Expr.t(), + calculation :: module | {module, Keyword.t()} | Ash.Expr.expression(), opts :: Keyword.t() ) :: Spark.Dsl.Builder.result() diff --git a/lib/ash/resource/change/builtins.ex b/lib/ash/resource/change/builtins.ex index 8069352c3b..38664762fe 100644 --- a/lib/ash/resource/change/builtins.ex +++ b/lib/ash/resource/change/builtins.ex @@ -14,7 +14,7 @@ defmodule Ash.Resource.Change.Builtins do This ensures that only things matching the provided filter are updated or destroyed. """ - @spec filter(expr :: Ash.Expr.t()) :: Ash.Resource.Change.ref() + @spec filter(expr :: Ash.Expr.expression()) :: Ash.Resource.Change.ref() def filter(filter) do {Ash.Resource.Change.Filter, filter: filter} end @@ -201,7 +201,7 @@ defmodule Ash.Resource.Change.Builtins do ] end """ - @spec atomic_set(attribute :: atom, expr :: Ash.Expr.t(), opts :: Keyword.t()) :: + @spec atomic_set(attribute :: atom, expr :: Ash.Expr.expression(), opts :: Keyword.t()) :: Ash.Resource.Change.ref() def atomic_set(attribute, expr, opts \\ []) do {Ash.Resource.Change.AtomicSet, @@ -242,7 +242,7 @@ defmodule Ash.Resource.Change.Builtins do {:atomic, %{view_count: expr(view_count + 1)}} end """ - @spec atomic_update(attribute :: atom, expr :: Ash.Expr.t(), opts :: Keyword.t()) :: + @spec atomic_update(attribute :: atom, expr :: Ash.Expr.expression(), opts :: Keyword.t()) :: Ash.Resource.Change.ref() def atomic_update(attribute, expr, opts \\ []) do {Ash.Resource.Change.Atomic, diff --git a/lib/ash/resource/change/change.ex b/lib/ash/resource/change/change.ex index 612b1fdd6d..693a416c04 100644 --- a/lib/ash/resource/change/change.ex +++ b/lib/ash/resource/change/change.ex @@ -128,14 +128,14 @@ defmodule Ash.Resource.Change do @doc false @spec atomic(module(), Ash.Changeset.t(), Keyword.t(), Ash.Resource.Change.Context.t()) :: {:ok, Ash.Changeset.t()} - | {:atomic, %{optional(atom()) => Ash.Expr.t() | {:atomic, Ash.Expr.t()}}} - | {:atomic, Ash.Changeset.t(), %{optional(atom()) => Ash.Expr.t()}} - | {:atomic, Ash.Changeset.t(), %{optional(atom()) => Ash.Expr.t()}, list()} - | {:atomic, %{optional(atom()) => Ash.Expr.t()}, list()} - | {:atomic_set, %{optional(atom()) => Ash.Expr.t() | {:atomic, Ash.Expr.t()}}} + | {:atomic, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} + | {:atomic, Ash.Changeset.t(), %{optional(atom()) => Ash.Expr.expression()}} + | {:atomic, Ash.Changeset.t(), %{optional(atom()) => Ash.Expr.expression()}, list()} + | {:atomic, %{optional(atom()) => Ash.Expr.expression()}, list()} + | {:atomic_set, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} | list( - {:atomic, %{optional(atom()) => Ash.Expr.t() | {:atomic, Ash.Expr.t()}}} - | {:atomic_set, %{optional(atom()) => Ash.Expr.t() | {:atomic, Ash.Expr.t()}}} + {:atomic, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} + | {:atomic_set, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} ) | {:not_atomic, String.t()} | :ok @@ -408,22 +408,22 @@ defmodule Ash.Resource.Change do """ @callback atomic(changeset :: Ash.Changeset.t(), opts :: Keyword.t(), context :: Context.t()) :: {:ok, Ash.Changeset.t()} - | {:atomic, %{optional(atom()) => Ash.Expr.t() | {:atomic, Ash.Expr.t()}}} - | {:atomic, Ash.Changeset.t(), %{optional(atom()) => Ash.Expr.t()}} - | {:atomic, Ash.Changeset.t(), %{optional(atom()) => Ash.Expr.t()}, + | {:atomic, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} + | {:atomic, Ash.Changeset.t(), %{optional(atom()) => Ash.Expr.expression()}} + | {:atomic, Ash.Changeset.t(), %{optional(atom()) => Ash.Expr.expression()}, list( - {:atomic, involved_fields :: list(atom) | :*, condition_expr :: Ash.Expr.t(), - error_expr :: Ash.Expr.t()} + {:atomic, involved_fields :: list(atom) | :*, condition_expr :: Ash.Expr.expression(), + error_expr :: Ash.Expr.expression()} )} - | {:atomic, %{optional(atom()) => Ash.Expr.t()}, + | {:atomic, %{optional(atom()) => Ash.Expr.expression()}, list( - {:atomic, involved_fields :: list(atom) | :*, condition_expr :: Ash.Expr.t(), - error_expr :: Ash.Expr.t()} + {:atomic, involved_fields :: list(atom) | :*, condition_expr :: Ash.Expr.expression(), + error_expr :: Ash.Expr.expression()} )} - | {:atomic_set, %{optional(atom()) => Ash.Expr.t() | {:atomic, Ash.Expr.t()}}} + | {:atomic_set, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} | list( - {:atomic, %{optional(atom()) => Ash.Expr.t() | {:atomic, Ash.Expr.t()}}} - | {:atomic_set, %{optional(atom()) => Ash.Expr.t() | {:atomic, Ash.Expr.t()}}} + {:atomic, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} + | {:atomic_set, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} ) | {:not_atomic, String.t()} | :ok diff --git a/lib/ash/resource/identity.ex b/lib/ash/resource/identity.ex index cdcc253545..e3eecb8027 100644 --- a/lib/ash/resource/identity.ex +++ b/lib/ash/resource/identity.ex @@ -99,7 +99,7 @@ defmodule Ash.Resource.Identity do name: atom(), keys: list(atom()), description: String.t() | nil, - where: nil | Ash.Expr.t(), + where: nil | Ash.Expr.expression(), nils_distinct?: boolean(), all_tenants?: boolean(), __spark_metadata__: Spark.Dsl.Entity.spark_meta() diff --git a/lib/ash/resource/validation.ex b/lib/ash/resource/validation.ex index 5820831a29..cf18c05f46 100644 --- a/lib/ash/resource/validation.ex +++ b/lib/ash/resource/validation.ex @@ -82,11 +82,11 @@ defmodule Ash.Resource.Validation do context :: Context.t() ) :: :ok - | {:atomic, involved_fields :: list(atom) | :*, condition_expr :: Ash.Expr.t(), - error_expr :: Ash.Expr.t()} + | {:atomic, involved_fields :: list(atom) | :*, condition_expr :: Ash.Expr.expression(), + error_expr :: Ash.Expr.expression()} | [ - {:atomic, involved_fields :: list(atom) | :*, condition_expr :: Ash.Expr.t(), - error_expr :: Ash.Expr.t()} + {:atomic, involved_fields :: list(atom) | :*, condition_expr :: Ash.Expr.expression(), + error_expr :: Ash.Expr.expression()} ] | {:not_atomic, String.t()} | {:error, term()} @@ -320,8 +320,8 @@ defmodule Ash.Resource.Validation do Context.t() ) :: :ok - | {:atomic, list(atom()) | :*, Ash.Expr.t(), Ash.Expr.t()} - | [{:atomic, list(atom()) | :*, Ash.Expr.t(), Ash.Expr.t()}] + | {:atomic, list(atom()) | :*, Ash.Expr.expression(), Ash.Expr.expression()} + | [{:atomic, list(atom()) | :*, Ash.Expr.expression(), Ash.Expr.expression()}] | {:not_atomic, String.t()} | {:error, term()} def atomic(module, changeset_query_or_input, opts, context) do diff --git a/lib/ash/type/type.ex b/lib/ash/type/type.ex index 738e364b3d..a7a219addf 100644 --- a/lib/ash/type/type.ex +++ b/lib/ash/type/type.ex @@ -485,16 +485,16 @@ defmodule Ash.Type do ``` """ - @callback cast_atomic(new_value :: Ash.Expr.t(), constraints) :: - {:atomic, Ash.Expr.t()} | {:error, Ash.Error.t()} | {:not_atomic, String.t()} + @callback cast_atomic(new_value :: Ash.Expr.expression(), constraints) :: + {:atomic, Ash.Expr.expression()} | {:error, Ash.Error.t()} | {:not_atomic, String.t()} @doc "Casts a list of values within an expression. See `c:cast_atomic/2` for more." - @callback cast_atomic_array(new_value :: Ash.Expr.t(), constraints) :: - {:atomic, Ash.Expr.t()} | {:error, Ash.Error.t()} | {:not_atomic, String.t()} + @callback cast_atomic_array(new_value :: Ash.Expr.expression(), constraints) :: + {:atomic, Ash.Expr.expression()} | {:error, Ash.Error.t()} | {:not_atomic, String.t()} @doc "Applies type constraints within an expression." - @callback apply_atomic_constraints(new_value :: Ash.Expr.t(), constraints) :: - :ok | {:ok, Ash.Expr.t()} | {:error, Ash.Error.t()} + @callback apply_atomic_constraints(new_value :: Ash.Expr.expression(), constraints) :: + :ok | {:ok, Ash.Expr.expression()} | {:error, Ash.Error.t()} @doc """ Whether or not a value with given constraints may support being cast atomic @@ -504,8 +504,8 @@ defmodule Ash.Type do @callback may_support_atomic_update?(constraints) :: boolean @doc "Applies type constraints to a list of values within an expression. See `c:apply_atomic_constraints/2` for more." - @callback apply_atomic_constraints_array(new_value :: Ash.Expr.t(), constraints) :: - :ok | {:ok, Ash.Expr.t()} | {:error, Ash.Error.t()} + @callback apply_atomic_constraints_array(new_value :: Ash.Expr.expression(), constraints) :: + :ok | {:ok, Ash.Expr.expression()} | {:error, Ash.Error.t()} @doc """ Return true if the type is a composite type, meaning it is made up of one or more values. How this works is up to the data layer. @@ -1332,7 +1332,7 @@ defmodule Ash.Type do This delegates to the underlying types implementation of `c:cast_atomic/2`. """ @spec cast_atomic(t(), term, constraints()) :: - {:atomic, Ash.Expr.t()} + {:atomic, Ash.Expr.expression()} | {:ok, term} | {:error, Ash.Error.t()} | {:not_atomic, String.t()} @@ -1379,7 +1379,7 @@ defmodule Ash.Type do This delegates to the underlying types implementation of `c:apply_atomic_constraints/2`. """ @spec apply_atomic_constraints(t(), term, constraints()) :: - {:ok, Ash.Expr.t()} | {:error, Ash.Error.t()} + {:ok, Ash.Expr.expression()} | {:error, Ash.Error.t()} def apply_atomic_constraints({:array, {:array, _}}, _term, _constraints), do: {:not_atomic, "cannot currently atomically update doubly nested arrays"} From 693fc1a2794f4a946d7e5917b9f0847ebbe531e0 Mon Sep 17 00:00:00 2001 From: Jinkyou Son Date: Fri, 3 Apr 2026 15:00:00 +0900 Subject: [PATCH 2/6] add %Ash.Expr{} struct with wrap/unwrap helpers --- lib/ash/expr/expr.ex | 25 ++++++++++++++++++------- lib/ash/sort/sort.ex | 1 - 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/ash/expr/expr.ex b/lib/ash/expr/expr.ex index b705ee0212..702599a7a6 100644 --- a/lib/ash/expr/expr.ex +++ b/lib/ash/expr/expr.ex @@ -6,11 +6,19 @@ defmodule Ash.Expr do @moduledoc "Tools to build Ash expressions" alias Ash.Query.{BooleanExpression, Not} + defstruct [:expr] + + @type t :: %__MODULE__{expr: any()} + @type expression :: t() | term() + + def wrap(%__MODULE__{} = already), do: already + def wrap(expr), do: %__MODULE__{expr: expr} + + def unwrap(%__MODULE__{expr: inner}), do: inner + def unwrap(other), do: other + @doc "Prepares a filter for comparison" defdelegate to_sat_expression(resource, expression), to: Ash.Expr.SAT - - @type t :: any - @type expression :: term() @pass_through_funcs [:where, :or_where, :expr, :@] @aggregate_kinds Ash.Query.Aggregate.kinds() @@ -50,6 +58,7 @@ defmodule Ash.Expr do @doc "Returns true if the value is or contains an expression" @spec expr?(term) :: boolean() + def expr?(%__MODULE__{}), do: true def expr?({:_actor, _}), do: true def expr?({:_arg, _}), do: true def expr?({:_ref, _, _}), do: true @@ -135,7 +144,6 @@ defmodule Ash.Expr do ) end - @spec where(Macro.t(), Macro.t()) :: t defmacro where(left, right) do quote do Ash.Query.BooleanExpression.optimized_new( @@ -146,7 +154,6 @@ defmodule Ash.Expr do end end - @spec or_where(Macro.t(), Macro.t()) :: t defmacro or_where(left, right) do quote do Ash.Query.BooleanExpression.optimized_new( @@ -169,7 +176,6 @@ defmodule Ash.Expr do ]) ``` """ - @spec calc(Macro.t(), opts :: Keyword.t()) :: t() defmacro calc(expression, opts \\ []) do quote generated: true do require Ash.Expr @@ -194,7 +200,6 @@ defmodule Ash.Expr do @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)) @@ -2001,4 +2006,10 @@ defmodule Ash.Expr do defp remove_pin({:^, _, [value]}), do: value defp remove_pin(value), do: value + + defimpl Inspect do + def inspect(%Ash.Expr{expr: inner}, opts) do + Inspect.inspect(inner, opts) + end + end end diff --git a/lib/ash/sort/sort.ex b/lib/ash/sort/sort.ex index 8f422c313f..22833b398a 100644 --- a/lib/ash/sort/sort.ex +++ b/lib/ash/sort/sort.ex @@ -35,7 +35,6 @@ defmodule Ash.Sort do Ash.Query.sort(query, [{Ash.Sort.expr_sort(author.full_name, :string), :desc_nils_first}]) ``` """ - @spec expr_sort(Ash.Expr.t(), Ash.Type.t() | nil) :: Ash.Expr.t() defmacro expr_sort(expression, type \\ nil) do quote generated: true do require Ash.Expr From 9b5289de5c76b67d617ee38fb2c59d3c92700743 Mon Sep 17 00:00:00 2001 From: Jinkyou Son Date: Fri, 3 Apr 2026 15:11:48 +0900 Subject: [PATCH 3/6] unwrap %Ash.Expr{} in expression traversal functions --- lib/ash/changeset/changeset.ex | 4 ++ lib/ash/expr/expr.ex | 19 +++++++ lib/ash/expr/sat.ex | 1 + lib/ash/filter/filter.ex | 66 +++++++++++++++++++++++++ lib/ash/filter/runtime.ex | 4 ++ lib/ash/policy/authorizer/authorizer.ex | 5 +- lib/ash/query/boolean_expression.ex | 6 +++ lib/ash/query/not.ex | 2 + 8 files changed, 106 insertions(+), 1 deletion(-) diff --git a/lib/ash/changeset/changeset.ex b/lib/ash/changeset/changeset.ex index 5c230d5964..2d015423de 100644 --- a/lib/ash/changeset/changeset.ex +++ b/lib/ash/changeset/changeset.ex @@ -2607,6 +2607,10 @@ defmodule Ash.Changeset do end end + defp check_for_exists_with_relationships(%Ash.Expr{expr: inner}) do + check_for_exists_with_relationships(inner) + end + defp check_for_exists_with_relationships(%Ash.Query.Exists{path: path}) when path != [] do {:error, "atomic_set cannot use exists with relationships"} end diff --git a/lib/ash/expr/expr.ex b/lib/ash/expr/expr.ex index 702599a7a6..145ba359bb 100644 --- a/lib/ash/expr/expr.ex +++ b/lib/ash/expr/expr.ex @@ -348,6 +348,10 @@ defmodule Ash.Expr do def can_return_nil?(nil), do: true + def can_return_nil?(%__MODULE__{expr: inner}) do + can_return_nil?(inner) + end + def can_return_nil?(%Ash.Query.BooleanExpression{left: left, right: right}) do can_return_nil?(left) || can_return_nil?(right) end @@ -379,6 +383,10 @@ defmodule Ash.Expr do end @doc "Whether or not a given template contains an actor reference" + def template_references?(%__MODULE__{expr: inner}, pred) do + template_references?(inner, pred) + end + def template_references?(%{__struct__: Ash.Filter, expression: expression}, pred) do template_references?(expression, pred) end @@ -436,6 +444,17 @@ defmodule Ash.Expr do def template_references?(thing, pred), do: pred.(thing) @doc false + def walk_template(%__MODULE__{expr: inner} = wrapper, mapper) do + case mapper.(wrapper) do + ^wrapper -> + walked = walk_template(inner, mapper) + wrap(walked) + + other -> + walk_template(other, mapper) + end + end + def walk_template(filter, mapper) when is_list(filter) do case mapper.(filter) do ^filter -> diff --git a/lib/ash/expr/sat.ex b/lib/ash/expr/sat.ex index 0cbc6f39d7..bf12f26fab 100644 --- a/lib/ash/expr/sat.ex +++ b/lib/ash/expr/sat.ex @@ -24,6 +24,7 @@ defmodule Ash.Expr.SAT do defp filter_to_expr(nil), do: nil defp filter_to_expr(false), do: false defp filter_to_expr(true), do: true + defp filter_to_expr(%Ash.Expr{expr: inner}), do: filter_to_expr(inner) defp filter_to_expr(%Filter{expression: expression}), do: filter_to_expr(expression) defp filter_to_expr(%{__predicate__?: _} = op_or_func), do: op_or_func defp filter_to_expr(%Ash.Query.Exists{} = exists), do: exists diff --git a/lib/ash/filter/filter.ex b/lib/ash/filter/filter.ex index 0046eee003..426dd92903 100644 --- a/lib/ash/filter/filter.ex +++ b/lib/ash/filter/filter.ex @@ -696,6 +696,10 @@ defmodule Ash.Filter do do_find(expr, pred, false, ors?, ands?, structures?) end + defp do_find(%Ash.Expr{expr: inner}, pred, value?, ors?, ands?, structures?) do + do_find(inner, pred, value?, ors?, ands?, structures?) + end + defp do_find(expr, pred, value?, ors?, ands?, structures?) do if value = pred.(expr) do if value? do @@ -755,6 +759,9 @@ defmodule Ash.Filter do defp get_predicates(expr, skip_invalid?, acc \\ []) + defp get_predicates(%Ash.Expr{expr: inner}, skip_invalid?, acc), + do: get_predicates(inner, skip_invalid?, acc) + defp get_predicates(true, _skip_invalid?, acc), do: acc defp get_predicates(false, _, _), do: false defp get_predicates(_, _, false), do: false @@ -1550,6 +1557,9 @@ defmodule Ash.Filter do {:halt, expr} -> expr + %Ash.Expr{expr: inner} = wrapper -> + %{wrapper | expr: map(inner, func)} + value when is_tuple(value) -> value |> Tuple.to_list() @@ -1632,6 +1642,9 @@ defmodule Ash.Filter do defp do_flat_map(expression, func) do case expression do + %Ash.Expr{expr: inner} -> + flat_map(inner, func) + %BooleanExpression{left: left, right: right} -> func.(expression) ++ flat_map(left, func) ++ flat_map(right, func) @@ -1691,6 +1704,9 @@ defmodule Ash.Filter do def update_aggregates(expression, resource, mapper, nested_path, parent_paths) do case expression do + %Ash.Expr{expr: inner} -> + update_aggregates(inner, resource, mapper, nested_path, parent_paths) + {key, value} when is_atom(key) -> {key, update_aggregates(value, resource, mapper, nested_path, parent_paths)} @@ -1777,6 +1793,15 @@ defmodule Ash.Filter do def run_other_data_layer_filters(_, _, filter, _tenant) when filter in [nil, true, false], do: {:ok, filter} + defp do_run_other_data_layer_filters( + %Ash.Expr{expr: inner}, + domain, + resource, + tenant + ) do + do_run_other_data_layer_filters(inner, domain, resource, tenant) + end + defp do_run_other_data_layer_filters( %BooleanExpression{op: op, left: left, right: right}, domain, @@ -1866,6 +1891,10 @@ defmodule Ash.Filter do defp do_run_other_data_layer_filters(other, _domain, _resource, _data), do: {:ok, other} + defp split_expression_by_relationship_path(%Ash.Expr{expr: inner}, path) do + split_expression_by_relationship_path(inner, path) + end + defp split_expression_by_relationship_path(%{expression: expression}, path) do split_expression_by_relationship_path(expression, path) end @@ -1926,6 +1955,10 @@ defmodule Ash.Filter do end end + defp scope_refs(%Ash.Expr{expr: inner}, path) do + scope_refs(inner, path) + end + defp scope_refs(%BooleanExpression{left: left, right: right} = expr, path) do %{expr | left: scope_refs(left, path), right: scope_refs(right, path)} end @@ -2212,6 +2245,15 @@ defmodule Ash.Filter do end end + defp do_relationship_paths( + %Ash.Expr{expr: inner}, + include_exists?, + with_references?, + expand_aggregates? + ) do + do_relationship_paths(inner, include_exists?, with_references?, expand_aggregates?) + end + defp do_relationship_paths( %Ref{ relationship_path: path, @@ -2650,6 +2692,16 @@ defmodule Ash.Filter do ) end + defp do_list_refs( + %Ash.Expr{expr: inner}, + no_longer_simple?, + in_an_eq?, + expand_calculations?, + expand_get_path? + ) do + do_list_refs(inner, no_longer_simple?, in_an_eq?, expand_calculations?, expand_get_path?) + end + defp do_list_refs( {key, value}, no_longer_simple?, @@ -2796,6 +2848,9 @@ defmodule Ash.Filter do def list_predicates(expression) do case expression do + %Ash.Expr{expr: inner} -> + list_predicates(inner) + %BooleanExpression{left: left, right: right} -> list_predicates(left) ++ list_predicates(right) @@ -2899,6 +2954,9 @@ defmodule Ash.Filter do end end + defp parse_expression(%Ash.Expr{expr: inner}, context), + do: parse_expression(inner, context) + defp parse_expression(%__MODULE__{expression: expression}, context), do: {:ok, move_to_relationship_path(expression, context[:relationship_path] || [])} @@ -2920,6 +2978,10 @@ defmodule Ash.Filter do defp add_expression_part(boolean, context, expression, could_be_function? \\ true) + defp add_expression_part(%Ash.Expr{expr: inner}, context, expression, could_be_function?) do + add_expression_part(inner, context, expression, could_be_function?) + end + defp add_expression_part(boolean, context, nil, _) do add_expression_part(boolean, context, true) end @@ -4012,6 +4074,10 @@ defmodule Ash.Filter do do_hydrate_refs(value, context) end + def do_hydrate_refs(%Ash.Expr{expr: inner}, context) do + do_hydrate_refs(inner, context) + end + def do_hydrate_refs(%__MODULE__{expression: expression} = filter, context) do with {:ok, expr} <- do_hydrate_refs(expression, context) do {:ok, %{filter | expression: expr}} diff --git a/lib/ash/filter/runtime.ex b/lib/ash/filter/runtime.ex index f34ab48ac6..48dd35feba 100644 --- a/lib/ash/filter/runtime.ex +++ b/lib/ash/filter/runtime.ex @@ -364,6 +364,10 @@ defmodule Ash.Filter.Runtime do end end + defp resolve_expr(%Ash.Expr{expr: inner}, record, parent, resource, unknown_on_unknown_refs?) do + resolve_expr(inner, record, parent, resource, unknown_on_unknown_refs?) + end + defp resolve_expr({:_actor, _}, _, _, _, _), do: :unknown defp resolve_expr({:_arg, _}, _, _, _, _), do: :unknown defp resolve_expr({:_ref, _}, _, _, _, _), do: :unknown diff --git a/lib/ash/policy/authorizer/authorizer.ex b/lib/ash/policy/authorizer/authorizer.ex index 1d65cf3649..50f7d1645c 100644 --- a/lib/ash/policy/authorizer/authorizer.ex +++ b/lib/ash/policy/authorizer/authorizer.ex @@ -795,7 +795,7 @@ defmodule Ash.Policy.Authorizer do {nil, %Ash.Query.Calculation{module: Ash.Resource.Calculation.Expression, opts: opts} = calc} -> field_and_path = - case opts[:expr] do + case Ash.Expr.unwrap(opts[:expr]) do %Ash.Query.Function.Type{arguments: [%Ash.Query.Ref{} = ref | _]} -> {ref.relationship_path, ref.attribute.name} @@ -970,6 +970,9 @@ defmodule Ash.Policy.Authorizer do defp replace_refs(expression, acc) do case expression do + %Ash.Expr{expr: inner} -> + replace_refs(inner, acc) + %Ash.Query.BooleanExpression{op: op, left: left, right: right} -> {left, acc} = replace_refs(left, acc) {right, acc} = replace_refs(right, acc) diff --git a/lib/ash/query/boolean_expression.ex b/lib/ash/query/boolean_expression.ex index 0cde2ce8ca..29ae23e4c3 100644 --- a/lib/ash/query/boolean_expression.ex +++ b/lib/ash/query/boolean_expression.ex @@ -11,6 +11,9 @@ defmodule Ash.Query.BooleanExpression do alias Ash.Query.Operator.{Eq, In, NotEq} alias Ash.Query.Ref + def new(op, %Ash.Expr{expr: inner}, right), do: new(op, inner, right) + def new(op, left, %Ash.Expr{expr: inner}), do: new(op, left, inner) + def new(_, nil, nil), do: false def new(:or, left, nil), do: left def new(:or, nil, right), do: right @@ -35,6 +38,9 @@ defmodule Ash.Query.BooleanExpression do # We may want to go down this route some day, but for now we simply use this to combine # statements where possible, which helps with authorization logic that leverages the query. # For example, `x in [1, 2, 3] and x != 1` becomes `x in [2, 3]` + def optimized_new(op, %Ash.Expr{expr: inner}, right), do: optimized_new(op, inner, right) + def optimized_new(op, left, %Ash.Expr{expr: inner}), do: optimized_new(op, left, inner) + def optimized_new(_, nil, nil), do: false def optimized_new(:and, false, _), do: false def optimized_new(:and, _, false), do: false diff --git a/lib/ash/query/not.ex b/lib/ash/query/not.ex index 5c0c434488..714c49de5a 100644 --- a/lib/ash/query/not.ex +++ b/lib/ash/query/not.ex @@ -6,6 +6,8 @@ defmodule Ash.Query.Not do @moduledoc "Represents the negation of the contained expression" defstruct [:expression] + def new(%Ash.Expr{expr: inner}), do: new(inner) + def new(nil), do: nil def new(%__MODULE__{expression: expression}), do: expression From 443be07575e342f10c3aae52b5363ab3d38f0c7e Mon Sep 17 00:00:00 2001 From: Jinkyou Son Date: Fri, 3 Apr 2026 17:07:51 +0900 Subject: [PATCH 4/6] return %Ash.Expr{} from expr/1, where/2, or_where/2 macros --- lib/ash/expr/expr.ex | 28 ++++++++---- test/expr_test.exs | 61 +++++++++++++++++++++++-- test/resource/unrelated_exists_test.exs | 4 +- 3 files changed, 77 insertions(+), 16 deletions(-) diff --git a/lib/ash/expr/expr.ex b/lib/ash/expr/expr.ex index 145ba359bb..beeece8161 100644 --- a/lib/ash/expr/expr.ex +++ b/lib/ash/expr/expr.ex @@ -145,21 +145,31 @@ defmodule Ash.Expr do end defmacro where(left, right) do + left_expr = do_expr(left) + right_expr = do_expr(right) + quote do - Ash.Query.BooleanExpression.optimized_new( - :and, - Ash.Expr.expr(unquote(left)), - Ash.Expr.expr(unquote(right)) + Ash.Expr.wrap( + Ash.Query.BooleanExpression.optimized_new( + :and, + unquote(left_expr), + unquote(right_expr) + ) ) end end defmacro or_where(left, right) do + left_expr = do_expr(left) + right_expr = do_expr(right) + quote do - Ash.Query.BooleanExpression.optimized_new( - :or, - Ash.Expr.expr(unquote(left)), - Ash.Expr.expr(unquote(right)) + Ash.Expr.wrap( + Ash.Query.BooleanExpression.optimized_new( + :or, + unquote(left_expr), + unquote(right_expr) + ) ) end end @@ -210,7 +220,7 @@ defmodule Ash.Expr do expr = do_expr(body) quote location: :keep do - unquote(expr) + Ash.Expr.wrap(unquote(expr)) end end diff --git a/test/expr_test.exs b/test/expr_test.exs index e2424696a5..00753ca58f 100644 --- a/test/expr_test.exs +++ b/test/expr_test.exs @@ -28,9 +28,9 @@ defmodule Ash.Test.ExprTest do describe "fragments" do test "allow pure binary sigils" do - assert expr(fragment(~SQL"? > ?", 2, 1)) = expr(fragment("? > ?", 2, 1)) - assert expr(fragment(~S"? > ?", 2, 1)) = expr(fragment("? > ?", 2, 1)) - assert expr(fragment(~s"? > ?", 2, 1)) = expr(fragment("? > ?", 2, 1)) + assert expr(fragment(~SQL"? > ?", 2, 1)) == expr(fragment("? > ?", 2, 1)) + assert expr(fragment(~S"? > ?", 2, 1)) == expr(fragment("? > ?", 2, 1)) + assert expr(fragment(~s"? > ?", 2, 1)) == expr(fragment("? > ?", 2, 1)) injection_ast = quote do @@ -200,7 +200,7 @@ defmodule Ash.Test.ExprTest do calc = calc(fragment("similarity(id, ?)", ^arg(:q)), type: :float) assert %Ash.Query.Call{name: :fragment, args: ["similarity(id, ?)", {:_arg, :q}]} = - calc.opts[:expr] + Ash.Expr.unwrap(calc.opts[:expr]) # Use the same mapper pattern that fill_template uses for {:_arg, field} args = %{q: "test_value"} @@ -220,7 +220,58 @@ defmodule Ash.Test.ExprTest do # The arg reference should now be resolved to the actual value assert %Ash.Query.Call{name: :fragment, args: ["similarity(id, ?)", "test_value"]} = - filled_calc.opts[:expr] + Ash.Expr.unwrap(filled_calc.opts[:expr]) + end + end + + describe "Ash.Expr struct" do + test "expr/1 returns %Ash.Expr{} for expression values" do + result = expr(1 + 2) + assert %Ash.Expr{} = result + end + + test "expr/1 wraps non-expression values too" do + x = 42 + result = expr(^x) + assert %Ash.Expr{expr: 42} = result + end + + test "expr?/1 returns true for %Ash.Expr{}" do + result = expr(x > 1) + assert expr?(result) + end + + test "wrap/1 prevents double-wrapping" do + result = expr(x > 1) + assert %Ash.Expr{} = result + double_wrapped = Ash.Expr.wrap(result) + assert double_wrapped === result + end + + test "unwrap/1 extracts inner expression" do + result = expr(x > 1) + inner = Ash.Expr.unwrap(result) + assert %Ash.Query.Call{name: :>, operator?: true} = inner + end + + test "unwrap/1 returns non-Ash.Expr values as-is" do + assert Ash.Expr.unwrap(42) == 42 + assert Ash.Expr.unwrap(nil) == nil + end + + test "Inspect protocol delegates to inner expression" do + result = expr(x > 1) + assert inspect(result) == inspect(Ash.Expr.unwrap(result)) + end + + test "eval works with %Ash.Expr{}" do + result = expr(1 + 2) + assert {:ok, 3} = Ash.Expr.eval(result) + end + + test "where/2 returns %Ash.Expr{}" do + result = where(x > 1, y < 10) + assert %Ash.Expr{} = result end end end diff --git a/test/resource/unrelated_exists_test.exs b/test/resource/unrelated_exists_test.exs index 1f64bd1455..900d06f367 100644 --- a/test/resource/unrelated_exists_test.exs +++ b/test/resource/unrelated_exists_test.exs @@ -622,14 +622,14 @@ defmodule Ash.Test.Resource.UnrelatedExistsTest do ) ) - assert %Ash.Query.Exists{related?: false} = expr_ast + assert %Ash.Expr{expr: %Ash.Query.Exists{related?: false} = exists_ast} = expr_ast assert %Ash.Query.BooleanExpression{ right: %Ash.Query.Exists{ related?: false, resource: Ash.Test.Resource.UnrelatedExistsTest.Report } - } = expr_ast.expr + } = exists_ast.expr end test "unrelated exists with aggregate comparisons" do From b2bd8aa184f33be8239a70b1b146468e381b5bb9 Mon Sep 17 00:00:00 2001 From: Jinkyou Son Date: Fri, 3 Apr 2026 18:00:26 +0900 Subject: [PATCH 5/6] fix nil case --- lib/ash/expr/expr.ex | 1 + test/expr_test.exs | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/ash/expr/expr.ex b/lib/ash/expr/expr.ex index beeece8161..096d6dd1ab 100644 --- a/lib/ash/expr/expr.ex +++ b/lib/ash/expr/expr.ex @@ -12,6 +12,7 @@ defmodule Ash.Expr do @type expression :: t() | term() def wrap(%__MODULE__{} = already), do: already + def wrap(nil), do: nil def wrap(expr), do: %__MODULE__{expr: expr} def unwrap(%__MODULE__{expr: inner}), do: inner diff --git a/test/expr_test.exs b/test/expr_test.exs index 00753ca58f..9d377e35e1 100644 --- a/test/expr_test.exs +++ b/test/expr_test.exs @@ -230,12 +230,16 @@ defmodule Ash.Test.ExprTest do assert %Ash.Expr{} = result end - test "expr/1 wraps non-expression values too" do + test "expr/1 wraps non-expression values" do x = 42 result = expr(^x) assert %Ash.Expr{expr: 42} = result end + test "expr/1 does not wrap nil" do + assert expr(^nil) == nil + end + test "expr?/1 returns true for %Ash.Expr{}" do result = expr(x > 1) assert expr?(result) From bca2e5473de8b9aadada6c265c349d221de4d335 Mon Sep 17 00:00:00 2001 From: Jinkyou Son Date: Fri, 3 Apr 2026 18:00:32 +0900 Subject: [PATCH 6/6] mix format --- lib/ash/changeset/changeset.ex | 3 ++- lib/ash/resource/change/change.ex | 35 ++++++++++++++++++++----------- lib/ash/resource/validation.ex | 8 +++---- lib/ash/type/type.ex | 8 +++++-- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/lib/ash/changeset/changeset.ex b/lib/ash/changeset/changeset.ex index 2d015423de..30a86e9dc2 100644 --- a/lib/ash/changeset/changeset.ex +++ b/lib/ash/changeset/changeset.ex @@ -2427,7 +2427,8 @@ defmodule Ash.Changeset do - `fully_atomic_changeset/4` for creating fully atomic changesets - `change_attribute/3` for regular (non-atomic) attribute changes """ - @spec atomic_update(t(), atom(), {:atomic, Ash.Expr.expression()} | Ash.Expr.expression()) :: t() + @spec atomic_update(t(), atom(), {:atomic, Ash.Expr.expression()} | Ash.Expr.expression()) :: + t() def atomic_update(changeset, key, value, opts \\ []) def atomic_update(changeset, key, {:atomic, value}, _opts) do diff --git a/lib/ash/resource/change/change.ex b/lib/ash/resource/change/change.ex index 693a416c04..a9167604a9 100644 --- a/lib/ash/resource/change/change.ex +++ b/lib/ash/resource/change/change.ex @@ -128,14 +128,18 @@ defmodule Ash.Resource.Change do @doc false @spec atomic(module(), Ash.Changeset.t(), Keyword.t(), Ash.Resource.Change.Context.t()) :: {:ok, Ash.Changeset.t()} - | {:atomic, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} + | {:atomic, + %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} | {:atomic, Ash.Changeset.t(), %{optional(atom()) => Ash.Expr.expression()}} | {:atomic, Ash.Changeset.t(), %{optional(atom()) => Ash.Expr.expression()}, list()} | {:atomic, %{optional(atom()) => Ash.Expr.expression()}, list()} - | {:atomic_set, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} + | {:atomic_set, + %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} | list( - {:atomic, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} - | {:atomic_set, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} + {:atomic, + %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} + | {:atomic_set, + %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} ) | {:not_atomic, String.t()} | :ok @@ -408,22 +412,29 @@ defmodule Ash.Resource.Change do """ @callback atomic(changeset :: Ash.Changeset.t(), opts :: Keyword.t(), context :: Context.t()) :: {:ok, Ash.Changeset.t()} - | {:atomic, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} + | {:atomic, + %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} | {:atomic, Ash.Changeset.t(), %{optional(atom()) => Ash.Expr.expression()}} | {:atomic, Ash.Changeset.t(), %{optional(atom()) => Ash.Expr.expression()}, list( - {:atomic, involved_fields :: list(atom) | :*, condition_expr :: Ash.Expr.expression(), - error_expr :: Ash.Expr.expression()} + {:atomic, involved_fields :: list(atom) | :*, + condition_expr :: Ash.Expr.expression(), error_expr :: Ash.Expr.expression()} )} | {:atomic, %{optional(atom()) => Ash.Expr.expression()}, list( - {:atomic, involved_fields :: list(atom) | :*, condition_expr :: Ash.Expr.expression(), - error_expr :: Ash.Expr.expression()} + {:atomic, involved_fields :: list(atom) | :*, + condition_expr :: Ash.Expr.expression(), error_expr :: Ash.Expr.expression()} )} - | {:atomic_set, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} + | {:atomic_set, + %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} | list( - {:atomic, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} - | {:atomic_set, %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} + {:atomic, + %{optional(atom()) => Ash.Expr.expression() | {:atomic, Ash.Expr.expression()}}} + | {:atomic_set, + %{ + optional(atom()) => + Ash.Expr.expression() | {:atomic, Ash.Expr.expression()} + }} ) | {:not_atomic, String.t()} | :ok diff --git a/lib/ash/resource/validation.ex b/lib/ash/resource/validation.ex index cf18c05f46..f89d652e3b 100644 --- a/lib/ash/resource/validation.ex +++ b/lib/ash/resource/validation.ex @@ -82,11 +82,11 @@ defmodule Ash.Resource.Validation do context :: Context.t() ) :: :ok - | {:atomic, involved_fields :: list(atom) | :*, condition_expr :: Ash.Expr.expression(), - error_expr :: Ash.Expr.expression()} + | {:atomic, involved_fields :: list(atom) | :*, + condition_expr :: Ash.Expr.expression(), error_expr :: Ash.Expr.expression()} | [ - {:atomic, involved_fields :: list(atom) | :*, condition_expr :: Ash.Expr.expression(), - error_expr :: Ash.Expr.expression()} + {:atomic, involved_fields :: list(atom) | :*, + condition_expr :: Ash.Expr.expression(), error_expr :: Ash.Expr.expression()} ] | {:not_atomic, String.t()} | {:error, term()} diff --git a/lib/ash/type/type.ex b/lib/ash/type/type.ex index a7a219addf..55050654f4 100644 --- a/lib/ash/type/type.ex +++ b/lib/ash/type/type.ex @@ -486,11 +486,15 @@ defmodule Ash.Type do """ @callback cast_atomic(new_value :: Ash.Expr.expression(), constraints) :: - {:atomic, Ash.Expr.expression()} | {:error, Ash.Error.t()} | {:not_atomic, String.t()} + {:atomic, Ash.Expr.expression()} + | {:error, Ash.Error.t()} + | {:not_atomic, String.t()} @doc "Casts a list of values within an expression. See `c:cast_atomic/2` for more." @callback cast_atomic_array(new_value :: Ash.Expr.expression(), constraints) :: - {:atomic, Ash.Expr.expression()} | {:error, Ash.Error.t()} | {:not_atomic, String.t()} + {:atomic, Ash.Expr.expression()} + | {:error, Ash.Error.t()} + | {:not_atomic, String.t()} @doc "Applies type constraints within an expression." @callback apply_atomic_constraints(new_value :: Ash.Expr.expression(), constraints) ::