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 !