Skip to content

Commit

Permalink
fix: handle related non-expr calculations referenced from expr calcs
Browse files Browse the repository at this point in the history
  • Loading branch information
zachdaniel committed Jan 17, 2025
1 parent 2102b05 commit b917bea
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 27 deletions.
42 changes: 23 additions & 19 deletions lib/ash/filter/runtime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -892,30 +892,34 @@ defmodule Ash.Filter.Runtime do
|> resolve_expr(record, parent, resource, unknown_on_unknown_refs?)
end
else
# We need to rewrite this
# As it stands now, it will evaluate the calculation
# once per expanded result. I'm not sure what that will
# look like though.
if unknown_on_unknown_refs? do
:unknown
else
# We need to rewrite this
# As it stands now, it will evaluate the calculation
# once per expanded result. I'm not sure what that will
# look like though.

if record do
case module.calculate([record], opts, context) do
[result] ->
{:ok, result}
if record do
case module.calculate([record], opts, context) do
[result] ->
{:ok, result}

{:ok, [result]} ->
{:ok, result}
{:ok, [result]} ->
{:ok, result}

:unknown when unknown_on_unknown_refs? ->
:unknown
:unknown when unknown_on_unknown_refs? ->

Check warning on line 911 in lib/ash/filter/runtime.ex

View workflow job for this annotation

GitHub Actions / ash-ci (SimpleSat) / mix dialyzer

guard_fail

The guard clause can never succeed.
:unknown

_ ->
{:ok, nil}
end
else
if unknown_on_unknown_refs? do
:unknown
_ ->
{:ok, nil}
end
else
{:ok, nil}
if unknown_on_unknown_refs? do
:unknown
else
{:ok, nil}
end
end
end
end
Expand Down
39 changes: 32 additions & 7 deletions lib/ash/resource/calculation/expression.ex
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,16 @@ defmodule Ash.Resource.Calculation.Expression do
{:ok, expression} ->
expression
|> Ash.Filter.list_refs()
|> Enum.map(fn
%{attribute: %Ash.Query.Aggregate{} = agg} ->
agg
|> Enum.reduce([], fn
%{attribute: %Ash.Query.Aggregate{} = agg, relationship_path: relationship_path}, acc ->
add_at_path(acc, relationship_path, agg)

%{attribute: %Ash.Query.Calculation{} = calc} ->
calc
%{attribute: %Ash.Query.Calculation{} = calc, relationship_path: relationship_path},
acc ->
add_at_path(acc, relationship_path, calc)

%{attribute: %{name: name}} ->
name
%{attribute: %{name: name}, relationship_path: relationship_path}, acc ->
add_at_path(acc, relationship_path, name)
end)
|> Enum.concat(Ash.Filter.used_aggregates(expression))
|> Enum.uniq()
Expand All @@ -130,4 +131,28 @@ defmodule Ash.Resource.Calculation.Expression do
[]
end
end

defp add_at_path(acc, [], value) do
Enum.uniq([value | acc])
end

defp add_at_path(acc, [first | rest], value) do
Enum.reduce(acc, {acc, false}, fn
{key, current}, {acc, _found?} when key == first ->
{[{key, add_at_path(List.wrap(current), rest, value)} | acc], true}

key, {acc, _found?} when key == first ->
{[{key, add_at_path([], rest, value)} | acc], true}

v, {acc, found?} ->
{[v | acc], found?}
end)
|> case do
{acc, false} ->
[{first, add_at_path([], rest, value)} | acc]

{acc, _} ->
acc
end
end
end
5 changes: 4 additions & 1 deletion lib/ash/resource/calculation/load_relationship.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ defmodule Ash.Resource.Calculation.LoadRelationship do
end

load_opts =
Ash.Context.to_opts(context, Keyword.put(opts[:opts] || [], :domain, opts[:domain]))
Ash.Context.to_opts(
context,
Keyword.put(opts[:opts] || [], :domain, opts[:domain])
)

Ash.load(results, [{relationship.name, query}], load_opts)
|> case do
Expand Down
102 changes: 102 additions & 0 deletions test/calculations/expr_calculation_with_related_dep_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
defmodule Ash.Test.ExprCalculationWithRelatedDepTest do
@moduledoc false
use ExUnit.Case, async: true

defmodule Balance do
use Ash.Resource.Calculation

@impl Ash.Resource.Calculation
def load(_, _, _), do: :balance_attr

@impl Ash.Resource.Calculation
def calculate(accounts, _, context) do
opts = Ash.Context.to_opts(context)

if opts[:actor] != %{a: :b} do
raise "actor not correct"
end

if opts[:authorize?] do
raise "should not be authorizing"
end

{:ok, Enum.map(accounts, fn account -> account.balance_attr end)}
end
end

defmodule Account2 do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
domain: Ash.Test.Domain

ets do
private? true
end

actions do
defaults([:read, create: :*])
end

attributes do
uuid_primary_key(:id)

attribute(:type, :string, public?: true)
attribute(:balance_attr, :integer, public?: true)

timestamps()
end

calculations do
calculate :balance, :integer, Balance
end

relationships do
belongs_to :account, Account do
public? true
allow_nil? false
end
end
end

defmodule Account do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
domain: Ash.Test.Domain

ets do
private? true
end

actions do
defaults([:read, create: :*])
end

attributes do
uuid_primary_key(:id)

attribute(:type, :string, public?: true)

timestamps()
end

calculations do
calculate(:balance, :integer, expr(related_account.balance))
end

relationships do
has_one :related_account, Account2 do
public?(true)
destination_attribute(:account_id)
end
end
end

test "can load non-expression calculations from expressions" do
account2 =
Ash.Seed.seed!(Account2, %{type: :test, balance_attr: 10})

account = Ash.Seed.seed!(Account, %{related_account: account2})

assert Ash.load!(account, :balance, authorize?: true, actor: %{a: :b}).balance == 10
end
end

0 comments on commit b917bea

Please sign in to comment.