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