Getting calculate/3 is undefined when calling my custom calculation that implements expression/2

Hey there, I have a calculation that I implemented the expression/2 callback:

defmodule Core.Pacman.Markets.Entity.Calculations.Purchases do @moduledoc false alias Core.Pacman.Markets.Entity.Calculations.Helper use Ash.Resource.Calculation import Core.Ash.Macros.ExprMacros @impl true def expression(_opts, context) do %{arguments: %{builder: builder}} = context distance = Helper.distance(builder) geo_point = builder |> Helper.geo_point() |> maybe_geo_point(distance) {start_date, end_date} = Helper.buy_interval(builder) strategies = Helper.strategies(builder) {min_beds, max_beds} = Helper.beds(builder) {min_baths, max_baths} = Helper.baths(builder) {min_sqrt, max_sqrt} = Helper.building_area(builder) {min_price, max_price} = Helper.buy_price(builder) zip_code = builder |> Helper.zip_code() |> maybe_filter_by_zip_code() financing_type = builder |> Helper.financing_type() |> maybe_filter_by_financing_type() filters = [ expr(strategy in ^strategies), expr_between(:buy_date, start_date, end_date), expr_between(:buy_price, min_price, max_price), expr_between(:property_bedrooms, min_beds, max_beds), expr_between(:property_building_area, min_sqrt, max_sqrt), expr_between(:property_bathrooms, min_baths, max_baths) ] ++ financing_type ++ geo_point ++ zip_code expr(count(:transactions, query: [filter: ^filters])) end defp maybe_filter_by_financing_type([]), do: [] defp maybe_filter_by_financing_type(types), do: [expr(financing_type in ^types)] defp maybe_filter_by_zip_code(nil), do: [] defp maybe_filter_by_zip_code(zip_code), do: [expr(property_zip == ^zip_code)] defp maybe_geo_point(nil, _), do: [] defp maybe_geo_point(_, nil), do: [] defp maybe_geo_point(geo_point, distance), do: [expr_geo_within(:property_geography, ^geo_point, ^distance)] end 

But when I tried to load it like this: Ash.load!(entities, {:purchases, args}, actor: actor), I get the following error:

[error] Task #PID<0.5980.0> started from #PID<0.5964.0> terminating ** (Ash.Error.Unknown) Bread Crumbs: > Exception raised in: Core.Pacman.Markets.Entity.read Unknown Error * ** (UndefinedFunctionError) function Core.Pacman.Markets.Entity.Calculations.TotalProfit.calculate/3 is undefined or private (core 1.225.0) Core.Pacman.Markets.Entity.Calculations.TotalProfit.calculate/3 (core 1.225.0) Core.Pacman.Markets.Entity.Calculations.TotalProfit.calculate(...) (ash 3.5.36) lib/ash/actions/read/calculations.ex:601: Ash.Actions.Read.Calculations.with_trace/4 (ash 3.5.36) lib/ash/actions/read/calculations.ex:540: Ash.Actions.Read.Calculations.run_calculate/7 (ash 3.5.36) lib/ash/actions/read/calculations.ex:520: Ash.Actions.Read.Calculations.run_calculation/3 (ash 3.5.36) lib/ash/actions/read/calculations.ex:406: anonymous fn/3 in Ash.Actions.Read.Calculations.do_run_calcs/4 (ash 3.5.36) lib/ash/actions/read/calculations.ex:401: Ash.Actions.Read.Calculations.do_run_calcs/4 (ash 3.5.36) lib/ash/actions/read/calculations.ex:356: Ash.Actions.Read.Calculations.do_run_calculations/5 (ash 3.5.36) lib/ash/actions/read/read.ex:453: Ash.Actions.Read.do_run/3 (ash 3.5.36) lib/ash/actions/read/read.ex:86: anonymous fn/3 in Ash.Actions.Read.run/3 (ash 3.5.36) lib/ash/actions/read/read.ex:85: Ash.Actions.Read.run/3 (ash 3.5.36) lib/ash.ex:2517: Ash.load/3 (ash 3.5.36) lib/ash.ex:2386: Ash.load!/3 

I’m not sure why it is calling calculate/3 instead of expression/2

Hmm…yeah this looks like a bug with nested calculations that only contain expression/2 callbacks. I’d like to fix it but I’m busy as usual. If you can provide a reproduction I’ll take a look ASAP :person_bowing:

There you go:

Application.put_env(:ash, :validate_domain_resource_inclusion?, false) Application.put_env(:ash, :validate_domain_config_inclusion?, false) Mix.install([{:ash, "~> 3.0"}], consolidate_protocols: false) defmodule Calculation do use Ash.Resource.Calculation def expression(_, _) do filters = [ expr(name == "bla"), expr(name == "ble") ] expr(count(:profiles, query: [filter: ^filters])) end end defmodule Tutorial.Profile2 do use Ash.Resource, domain: Tutorial, data_layer: Ash.DataLayer.Ets actions do defaults [:read, create: :*] end attributes do uuid_primary_key :id end relationships do belongs_to :profile, Tutorial.Profile, public?: true end end defmodule Tutorial.Profile do use Ash.Resource, domain: Tutorial, data_layer: Ash.DataLayer.Ets actions do defaults [:read, create: :*] end attributes do uuid_primary_key :id attribute :name, :string, public?: true end relationships do has_many :profiles, Tutorial.Profile2 end calculations do calculate :purchases, :integer do calculation Calculation end end end defmodule Tutorial do use Ash.Domain resources do resource Tutorial.Profile2 resource Tutorial.Profile end end p1 = Tutorial.Profile |> Ash.Changeset.for_create(:create, %{name: "John Doe"}) |> Ash.create!() p2 = Tutorial.Profile |> Ash.Changeset.for_create(:create, %{name: "John Doe"}) |> Ash.create!() pp1 = Tutorial.Profile2 |> Ash.Changeset.for_create(:create, %{profile_id: p1.id}) |> Ash.create!() pp2 = Tutorial.Profile2 |> Ash.Changeset.for_create(:create, %{profile_id: p1.id}) |> Ash.create!() pp3 = Tutorial.Profile2 |> Ash.Changeset.for_create(:create, %{profile_id: p2.id}) |> Ash.create!() [p1, p2] |> Ash.load(:purchases) 

Nice, could you also open an issue and put it there?

Done! Ash doesn't use calculation expression/2 call · Issue #2329 · ash-project/ash · GitHub

Hey @zachdaniel do you know any workaround for this to make it work until the issue is fixed (maybe some way to implement the calculate function that somehow calls the expression one)?

You should be able to just use that expression inline in the calculations instead of the module to workaround it for now. i.e calculate :foo, :type, expr(...)

Hmm, not sure if I can use that actually since that DSL expects a expr but my calculation actually creates the expr from the function params

Can you try this?

 def expression(_opts, context) do %{arguments: %{builder: builder}} = context distance = Helper.distance(builder) geo_point = builder |> Helper.geo_point() |> maybe_geo_point(distance) {start_date, end_date} = Helper.buy_interval(builder) strategies = Helper.strategies(builder) {min_beds, max_beds} = Helper.beds(builder) {min_baths, max_baths} = Helper.baths(builder) {min_sqrt, max_sqrt} = Helper.building_area(builder) {min_price, max_price} = Helper.buy_price(builder) zip_code = builder |> Helper.zip_code() |> maybe_filter_by_zip_code() financing_type = builder |> Helper.financing_type() |> maybe_filter_by_financing_type() filters = [ expr(strategy in ^strategies), expr_between(:buy_date, start_date, end_date), expr_between(:buy_price, min_price, max_price), expr_between(:property_bedrooms, min_beds, max_beds), expr_between(:property_building_area, min_sqrt, max_sqrt), expr_between(:property_bathrooms, min_baths, max_baths) ] ++ financing_type ++ geo_point ++ zip_code filter = Enum.reduce(tl(filters), hd(filters), fn filter, full_filter -> expr(^full_filter and ^filter) end) expr(count(transactions, query: [filter: ^filter])) end 

That didn’t work, but what work for me was actually changing my expr_between macro

 defmacro expr_between(attribute, low, high) do quote do import Ash.Expr require Ash.Expr cond do is_nil(unquote(low)) and not is_nil(unquote(high)) -> expr(^ref(unquote(attribute)) <= ^unquote(high)) not is_nil(unquote(low)) and is_nil(unquote(high)) -> expr(^ref(unquote(attribute)) >= ^unquote(low)) not is_nil(unquote(low)) and not is_nil(unquote(high)) -> expr( fragment( "(? between ? and ?)", ^ref(unquote(attribute)), ^unquote(low), ^unquote(high) ) ) true -> expr(true) end end end 

Basically it was missing the ^ when unquoting low and high values. After that, the calculation is working fine again

Okay, got it. So the problem is that the expression was invalid, and we’re trying to, as a last ditch effort, fall back to .calculate when we should instead display an error. Could you open an issue describing this?

Doesn’t the issue I created before ( Ash doesn't use calculation expression/2 call · Issue #2329 · ash-project/ash · GitHub ) already triggers this user case? At least the error it generates is the same one

Oh right :slight_smile: maybe just add a comment about what the issue is if you have a sec :heart:

done !