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
1 Like

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.

1 Like

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?

1 Like

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