Unsupported expression Elixir.AshPostgres.SqlImplementation query: {:_context, :locale}

I have a calculation like this:

defmodule MyApp.LocalisedName do
  use Ash.Resource.Calculation

  @impl true
  def load(_query, _opts, _context) do
    [trans: [:language, :name]]
  end

  @impl true
  def expression(_opts, _ctx) do
    expr(first(trans, [field: :name, filter: expr(language == ^context(:locale))]))
  end
end

where trans is a relationship for translations. Those have an “id”, a “name” and a “language”. My scope looks like this:

scope = %MyApp.Scope{current_user: user, current_tenant: :gr, locale: "de"}

this results in an error like this:

iex(17)> MyApp.Accounts.search_user("are", scope: scope)
** (Ash.Error.Unknown) 
Bread Crumbs:
  > Exception raised in: MyApp.Accounts.User.search

Unknown Error

* ** (RuntimeError) Unsupported expression in Elixir.AshPostgres.SqlImplementation query: {:_context, :locale}
  (ash_sql 0.3.15) lib/expr.ex:2721: AshSql.Expr.default_dynamic_expr/6
  (ash_sql 0.3.15) lib/expr.ex:2024: AshSql.Expr.default_dynamic_expr/6
  (ash_sql 0.3.15) lib/expr.ex:1380: anonymous fn/13 in AshSql.Expr.default_dynamic_expr/6
  (ash_sql 0.3.15) lib/filter.ex:42: anonymous fn/2 in AshSql.Filter.add_filter_expression/2
  (elixir 1.19.3) lib/enum.ex:2520: Enum."-reduce/3-lists^foldl/2-0-"/3
  (ash_sql 0.3.15) lib/filter.ex:26: AshSql.Filter.filter/4
  (ash_sql 0.3.15) lib/aggregate.ex:1371: anonymous fn/6 in AshSql.Aggregate.maybe_filter_subquery/6
  (elixir 1.19.3) lib/enum.ex:5023: Enumerable.List.reduce/3
  (elixir 1.19.3) lib/enum.ex:2574: Enum.reduce_while/3
  (ash_sql 0.3.15) lib/aggregate.ex:664: anonymous fn/10 in AshSql.Aggregate.get_subquery/12
  (ash_sql 0.3.15) lib/join.ex:368: AshSql.Join.related_subquery/3
  (ash_sql 0.3.15) lib/aggregate.ex:318: anonymous fn/7 in AshSql.Aggregate.add_aggregates/6
  (elixir 1.19.3) lib/enum.ex:5023: Enumerable.List.reduce/3
  (elixir 1.19.3) lib/enum.ex:2574: Enum.reduce_while/3
  (ash_sql 0.3.15) lib/aggregate.ex:157: AshSql.Aggregate.add_aggregates/6
  (ash_postgres 2.6.26) lib/data_layer.ex:3513: AshPostgres.DataLayer.filter/4
  (ash 3.10.0) lib/ash/query/query.ex:4752: Ash.Query.maybe_filter/3
  (ash 3.10.0) lib/ash/query/query.ex:4504: Ash.Query.data_layer_query/2
  (ash_sql 0.3.15) lib/join.ex:475: AshSql.Join.related_query/3
  (ash_sql 0.3.15) lib/join.ex:348: AshSql.Join.related_subquery/3
    (ash_sql 0.3.15) lib/expr.ex:2721: AshSql.Expr.default_dynamic_expr/6
    (ash_sql 0.3.15) lib/expr.ex:2024: AshSql.Expr.default_dynamic_expr/6
    (ash_sql 0.3.15) lib/expr.ex:1380: anonymous fn/13 in AshSql.Expr.default_dynamic_expr/6
    (ash_sql 0.3.15) lib/filter.ex:42: anonymous fn/2 in AshSql.Filter.add_filter_expression/2
    (elixir 1.19.3) lib/enum.ex:2520: Enum."-reduce/3-lists^foldl/2-0-"/3
    (ash_sql 0.3.15) lib/filter.ex:26: AshSql.Filter.filter/4
    (ash_sql 0.3.15) lib/aggregate.ex:1371: anonymous fn/6 in AshSql.Aggregate.maybe_filter_subquery/6
    (elixir 1.19.3) lib/enum.ex:5023: Enumerable.List.reduce/3
    (elixir 1.19.3) lib/enum.ex:2574: Enum.reduce_while/3
    (ash_sql 0.3.15) lib/aggregate.ex:664: anonymous fn/10 in AshSql.Aggregate.get_subquery/12
    (ash_sql 0.3.15) lib/join.ex:368: AshSql.Join.related_subquery/3
    (ash_sql 0.3.15) lib/aggregate.ex:318: anonymous fn/7 in AshSql.Aggregate.add_aggregates/6
    (elixir 1.19.3) lib/enum.ex:5023: Enumerable.List.reduce/3
    (elixir 1.19.3) lib/enum.ex:2574: Enum.reduce_while/3
    (ash_sql 0.3.15) lib/aggregate.ex:157: AshSql.Aggregate.add_aggregates/6
    (ash_postgres 2.6.26) lib/data_layer.ex:3513: AshPostgres.DataLayer.filter/4
    (ash 3.10.0) lib/ash/query/query.ex:4752: Ash.Query.maybe_filter/3
    (ash 3.10.0) lib/ash/query/query.ex:4504: Ash.Query.data_layer_query/2
    (ash_sql 0.3.15) lib/join.ex:475: AshSql.Join.related_query/3
    iex:17: (file)

If I do this:

expr(first(trans, [field: :name, filter: expr(language == "fr"))]))

then it works just fine.

I’m on the newest dependencies for:

  • ash
  • ash_postgres
  • ash_sql

You can get the context from context.source_context[:locale], templates don’t work inside of calculations because you can interpolate the actual values you need.

Also, no need to specify def load, the expression figures it out for you.

Yeah, that doesn’t work though. Given this expression:

expr(first(trans, [field: :name, filter: expr(language == ^ctx.source_context[:locale])]))

in the resulting query the locale is always empty (or nil or whatvever default one sets):

If I dbg(ctx) I can see the ctx many times in the logs, always with an empty source_context and only once with the actual context, with actor, tenant, etc. I get a lot of these:

[(ex_ebau 0.1.0) lib/ex_ebau/core/resource.ex:73: LocalisedName.expression/2]
ctx #=> %Ash.Resource.Calculation.Context{
  actor: nil,
  tenant: nil,
  authorize?: nil,
  tracer: nil,
  domain: nil,
  resource: nil,
  type: Ash.Type.String,
  constraints: [trim?: true, allow_empty?: false],
  arguments: %{locale: "de"},
  source_context: %{}
}

warning: Comparing values with `nil` will always return `false`. Use `is_nil/1` instead. In: `language == nil`
  (ex_ebau 0.1.0) lib/ex_ebau/core/resource.ex:74: LocalisedName.expression/2
  (ash 3.10.0) lib/ash/filter/filter.ex:2766: Ash.Filter.do_list_refs/5
  (elixir 1.19.3) lib/enum.ex:4497: Enum.flat_map_list/2
  (ash 3.10.0) lib/ash/filter/filter.ex:2752: Ash.Filter.do_list_refs/5
  (ash 3.10.0) lib/ash/filter/filter.ex:2698: Ash.Filter.do_list_refs/5
  (ash 3.10.0) lib/ash/filter/filter.ex:2645: Ash.Filter.list_refs/5
  (ash 3.10.0) lib/ash/filter/filter.ex:845: Ash.Filter.used_aggregates/3
  (ash 3.10.0) lib/ash/actions/read/read.ex:1451: anonymous fn/2 in Ash.Actions.Read.agg_refs/2
  (elixir 1.19.3) lib/enum.ex:4497: Enum.flat_map_list/2
  (ash 3.10.0) lib/ash/actions/read/read.ex:1440: Ash.Actions.Read.agg_refs/2
  (ash 3.10.0) lib/ash/actions/read/read.ex:701: anonymous fn/8 in Ash.Actions.Read.do_read/5
  (ash 3.10.0) lib/ash/actions/read/read.ex:1565: Ash.Actions.Read.maybe_in_transaction/3
  (ash 3.10.0) lib/ash/actions/read/read.ex:436: Ash.Actions.Read.do_run/3
  (ash 3.10.0) lib/ash/actions/read/read.ex:90: anonymous fn/3 in Ash.Actions.Read.run/3
  (ash 3.10.0) lib/ash/actions/read/read.ex:89: Ash.Actions.Read.run/3
  (ash 3.10.0) lib/ash.ex:2783: Ash.read/2
  (elixir 1.19.3) src/elixir.erl:365: :elixir.eval_external_handler/3
  (stdlib 6.2.2.2) erl_eval.erl:919: :erl_eval.do_apply/7

and only once I get:

[(ex_ebau 0.1.0) lib/ex_ebau/core/resource.ex:73: LocalisedName.expression/2]
ctx #=> %Ash.Resource.Calculation.Context{
  actor: %ExEbau.Accounts.User{
    id: 2,
    username: "34fc2a57-dec1-42d5-8831-9db8960ffbcb",
    email: "editor@example.com",
    name: "Applicant",
    surname: "Editor",
    language: "de",
    full_name: #Ash.NotLoaded<:calculation, field: :full_name>,
    groups_join_assoc: #Ash.NotLoaded<:relationship, field: :groups_join_assoc>,
    groups: #Ash.NotLoaded<:relationship, field: :groups>,
    user_groups: #Ash.NotLoaded<:relationship, field: :user_groups>,
    services: #Ash.NotLoaded<:relationship, field: :services>,
    active_instance_acls: #Ash.NotLoaded<:relationship, field: :active_instance_acls>,
    default_user_group: #Ash.NotLoaded<:relationship, field: :default_user_group>,
    group: #Ash.NotLoaded<:relationship, field: :group>,
    service: #Ash.NotLoaded<:relationship, field: :service>,
    meetings: #Ash.NotLoaded<:relationship, field: :meetings>,
    meetings_join_assoc: #Ash.NotLoaded<:relationship, field: :meetings_join_assoc>,
    __meta__: #Ecto.Schema.Metadata<:loaded, "USER">
  },
  tenant: :gr,
  authorize?: nil,
  tracer: nil,
  domain: nil,
  resource: nil,
  type: Ash.Type.String,
  constraints: [trim?: true, allow_empty?: false],
  arguments: %{locale: "de"},
  source_context: %{
    private: %{
      actor: %ExEbau.Accounts.User{
        id: 2,
        username: "34fc2a57-dec1-42d5-8831-9db8960ffbcb",
        email: "editor@example.com",
        name: "Applicant",
        surname: "Editor",
        language: "de",
        full_name: #Ash.NotLoaded<:calculation, field: :full_name>,
        groups_join_assoc: #Ash.NotLoaded<:relationship, field: :groups_join_assoc>,
        groups: #Ash.NotLoaded<:relationship, field: :groups>,
        user_groups: #Ash.NotLoaded<:relationship, field: :user_groups>,
        services: #Ash.NotLoaded<:relationship, field: :services>,
        active_instance_acls: #Ash.NotLoaded<:relationship, field: :active_instance_acls>,
        default_user_group: #Ash.NotLoaded<:relationship, field: :default_user_group>,
        group: #Ash.NotLoaded<:relationship, field: :group>,
        service: #Ash.NotLoaded<:relationship, field: :service>,
        meetings: #Ash.NotLoaded<:relationship, field: :meetings>,
        meetings_join_assoc: #Ash.NotLoaded<:relationship, field: :meetings_join_assoc>,
        __meta__: #Ecto.Schema.Metadata<:loaded, "USER">
      },
      tenant: :gr,
      authorizer_log?: false,
      authorize?: true,
      pre_flight_authorization?: false,
      in_before_action?: true
    },
    action: %Ash.Resource.Actions.Read{
      arguments: [
        %Ash.Resource.Actions.Argument{
          allow_nil?: false,
          type: Ash.Type.CiString,
          name: :search,
          default: nil,
          sensitive?: false,
          description: nil,
          public?: true,
          constraints: [trim?: true, allow_empty?: false, casing: nil],
          __spark_metadata__: %Spark.Dsl.Entity.Meta{
            anno: [file: ~c"/app/lib/ex_ebau/accounts/user.ex", location: 57],
            properties_anno: %{}
          }
        },
        %Ash.Resource.Actions.Argument{
          allow_nil?: true,
          type: Ash.Type.String,
          name: :locale,
          default: nil,
          sensitive?: false,
          description: nil,
          public?: true,
          constraints: [trim?: true, allow_empty?: false],
          __spark_metadata__: %Spark.Dsl.Entity.Meta{
            anno: [file: ~c"/app/lib/ex_ebau/accounts/user.ex", location: 58],
            properties_anno: %{}
          }
        }
      ],
      description: nil,
      filter: contains(full_name,  {:_arg, :search}) or exists(groups, contains(
        localised_name([locale: {:_arg, :locale}]), 
        {:_arg, :search}
      )),
      filters: [
        %Ash.Resource.Dsl.Filter{
          filter: contains(full_name,  {:_arg, :search}) or exists(groups, contains(
            localised_name([locale: {:_arg, :locale}]), 
            {:_arg, :search}
          )),
          __spark_metadata__: %Spark.Dsl.Entity.Meta{
            anno: [file: ~c"/app/lib/ex_ebau/accounts/user.ex", location: 71],
            properties_anno: %{}
          }
        }
      ],
      get_by: [],
      get?: false,
      manual: nil,
      metadata: [],
      skip_unknown_inputs: [],
      skip_global_validations?: false,
      modify_query: nil,
      multitenancy: :enforce,
      name: :search,
      pagination: false,
      preparations: [
        %Ash.Resource.Preparation{
          preparation: {Ash.Resource.Preparation.Function,
           [
             fun: &ExEbau.Accounts.User.preparation_0_generated_8CB389E4139DEDB84FF18190F5AC839B/2
           ]},
          only_when_valid?: false,
          where: [],
          on: [:read],
          __spark_metadata__: %Spark.Dsl.Entity.Meta{
            anno: [file: ~c"/app/lib/ex_ebau/accounts/user.ex", location: 60],
            properties_anno: %{}
          }
        }
      ],
      primary?: false,
      touches_resources: [],
      timeout: nil,
      transaction?: false,
      type: :read,
      __spark_metadata__: %Spark.Dsl.Entity.Meta{
        anno: [file: ~c"/app/lib/ex_ebau/accounts/user.ex", location: 56],
        properties_anno: %{}
      }
    },
    ...
  }
}

Interesting…that’s not ideal :thinking:

can you raise an error when that context isn’t set and show the stack trace? I think that’s something we can potentially address

Is this what you mean?

iex(18)> ExEbau.Accounts.search_user("foo", scope: scope)
[(ex_ebau 0.1.0) lib/ex_ebau/calulcations/localised_name.ex:11: ExEbau.Calculations.LocalisedName.expression/2]
ctx #=> %Ash.Resource.Calculation.Context{
  actor: nil,
  tenant: nil,
  authorize?: nil,
  tracer: nil,
  domain: nil,
  resource: nil,
  type: Ash.Type.String,
  constraints: [trim?: true, allow_empty?: false],
  arguments: %{},
  source_context: %{}
}

** (Ash.Error.Unknown) 
Bread Crumbs:
  > Exception raised in: ExEbau.Accounts.User.search

Unknown Error

* ** (KeyError) key :locale not found in:

    %{}

  (ex_ebau 0.1.0) lib/ex_ebau/calulcations/localised_name.ex:13: ExEbau.Calculations.LocalisedName.expression/2
  (ash 3.10.0) lib/ash/filter/filter.ex:2766: Ash.Filter.do_list_refs/5
  (elixir 1.19.3) lib/enum.ex:4497: Enum.flat_map_list/2
  (ash 3.10.0) lib/ash/filter/filter.ex:2752: Ash.Filter.do_list_refs/5
  (ash 3.10.0) lib/ash/filter/filter.ex:2698: Ash.Filter.do_list_refs/5
  (ash 3.10.0) lib/ash/filter/filter.ex:2645: Ash.Filter.list_refs/5
  (ash 3.10.0) lib/ash/filter/filter.ex:845: Ash.Filter.used_aggregates/3
  (ash 3.10.0) lib/ash/actions/read/read.ex:1451: anonymous fn/2 in Ash.Actions.Read.agg_refs/2
  (elixir 1.19.3) lib/enum.ex:4497: Enum.flat_map_list/2
  (ash 3.10.0) lib/ash/actions/read/read.ex:1440: Ash.Actions.Read.agg_refs/2
  (ash 3.10.0) lib/ash/actions/read/read.ex:701: anonymous fn/8 in Ash.Actions.Read.do_read/5
  (ash 3.10.0) lib/ash/actions/read/read.ex:1565: Ash.Actions.Read.maybe_in_transaction/3
  (ash 3.10.0) lib/ash/actions/read/read.ex:436: Ash.Actions.Read.do_run/3
  (ash 3.10.0) lib/ash/actions/read/read.ex:90: anonymous fn/3 in Ash.Actions.Read.run/3
  (ash 3.10.0) lib/ash/actions/read/read.ex:89: Ash.Actions.Read.run/3
  (ash 3.10.0) lib/ash.ex:2783: Ash.read/2
  (elixir 1.19.3) src/elixir.erl:365: :elixir.eval_external_handler/3
  (stdlib 7.1) erl_eval.erl:924: :erl_eval.do_apply/7
  (elixir 1.19.3) src/elixir.erl:343: :elixir.eval_forms/4
  (elixir 1.19.3) lib/module/parallel_checker.ex:155: Module.ParallelChecker.verify/1
    (ex_ebau 0.1.0) lib/ex_ebau/calulcations/localised_name.ex:13: ExEbau.Calculations.LocalisedName.expression/2
    (ash 3.10.0) lib/ash/filter/filter.ex:2766: Ash.Filter.do_list_refs/5
    (elixir 1.19.3) lib/enum.ex:4497: Enum.flat_map_list/2
    (ash 3.10.0) lib/ash/filter/filter.ex:2752: Ash.Filter.do_list_refs/5
    (ash 3.10.0) lib/ash/filter/filter.ex:2698: Ash.Filter.do_list_refs/5
    (ash 3.10.0) lib/ash/filter/filter.ex:2645: Ash.Filter.list_refs/5
    (ash 3.10.0) lib/ash/filter/filter.ex:845: Ash.Filter.used_aggregates/3
    (ash 3.10.0) lib/ash/actions/read/read.ex:1451: anonymous fn/2 in Ash.Actions.Read.agg_refs/2
    (elixir 1.19.3) lib/enum.ex:4497: Enum.flat_map_list/2
    (ash 3.10.0) lib/ash/actions/read/read.ex:1440: Ash.Actions.Read.agg_refs/2
    (ash 3.10.0) lib/ash/actions/read/read.ex:701: anonymous fn/8 in Ash.Actions.Read.do_read/5
    (ash 3.10.0) lib/ash/actions/read/read.ex:1565: Ash.Actions.Read.maybe_in_transaction/3
    (ash 3.10.0) lib/ash/actions/read/read.ex:436: Ash.Actions.Read.do_run/3
    (ash 3.10.0) lib/ash/actions/read/read.ex:90: anonymous fn/3 in Ash.Actions.Read.run/3
    (ash 3.10.0) lib/ash/actions/read/read.ex:89: Ash.Actions.Read.run/3
    (ash 3.10.0) lib/ash.ex:2783: Ash.read/2
    iex:18: (file)
iex(18)> 

Yes, exactly! I’m pretty sure I can do something about that :slight_smile: mind opening an issue?

done: Context doesn't get passed to calculations reliably · Issue #2458 · ash-project/ash · GitHub