Aggregate that requires complex joins

parent in an aggregate is the root resource, not the resource one up the aggregate relationship path. I would expect that to break more gracefully, but it would always break nonetheless.

Ah, thank you. That clarifies things.

Using the single parent now: filter expr(inserted_at == parent(last_read_message.inserted_at))

And got this:

     ** (Ash.Error.Unknown) 
     Bread Crumbs:
       > Exception raised in: ConversationUser.read

     Unknown Error

     * ** (RuntimeError) Error while building reference: last_read_message.inserted_at
       (ash_sql 0.2.59) lib/expr.ex:1915: AshSql.Expr.default_dynamic_expr/6
       (ash_sql 0.2.59) lib/expr.ex:2868: AshSql.Expr.maybe_type_expr/6
       (ash_sql 0.2.59) lib/expr.ex:1006: AshSql.Expr.default_dynamic_expr/6
       (ash_sql 0.2.59) lib/filter.ex:37: anonymous fn/2 in AshSql.Filter.add_filter_expression/2
       (elixir 1.17.2) lib/enum.ex:2531: Enum."-reduce/3-lists^foldl/2-0-"/3
       (ash_sql 0.2.59) lib/filter.ex:22: AshSql.Filter.filter/4
       (ash_sql 0.2.59) lib/aggregate.ex:887: anonymous fn/6 in AshSql.Aggregate.maybe_filter_subquery/6
       (elixir 1.17.2) lib/enum.ex:4858: Enumerable.List.reduce/3
       (elixir 1.17.2) lib/enum.ex:2585: Enum.reduce_while/3
       (ash_sql 0.2.59) lib/aggregate.ex:369: anonymous fn/10 in AshSql.Aggregate.add_aggregates/6
       (ash_sql 0.2.59) lib/join.ex:290: AshSql.Join.related_subquery/3
       (ash_sql 0.2.59) lib/aggregate.ex:217: anonymous fn/7 in AshSql.Aggregate.add_aggregates/6
       (elixir 1.17.2) lib/enum.ex:4858: Enumerable.List.reduce/3
       (elixir 1.17.2) lib/enum.ex:2585: Enum.reduce_while/3
       (ash_sql 0.2.59) lib/aggregate.ex:88: AshSql.Aggregate.add_aggregates/6
       (ash 3.4.66) lib/ash/query/query.ex:3157: Ash.Query.data_layer_query/2
       (ash 3.4.66) lib/ash/actions/read/read.ex:595: anonymous fn/8 in Ash.Actions.Read.do_read/5
       (ash 3.4.66) lib/ash/actions/read/read.ex:956: Ash.Actions.Read.maybe_in_transaction/3
       (ash 3.4.66) lib/ash/actions/read/read.ex:315: Ash.Actions.Read.do_run/3
       (ash 3.4.66) lib/ash/actions/read/read.ex:82: anonymous fn/3 in Ash.Actions.Read.run/3
defmodule ConversationUser do
  relationships do
    belongs_to :last_read_message, Message,
      source_attribute: :last_read_message_id,
      public?: true
  end

  aggregates do
    count :unread_messages_count2, [:conversation, :messages] do
      filter expr(inserted_at == parent(last_read_message.inserted_at))
    end
end

Ash.get!(ConversationUser, id,
  authorize?: false,
  load: [:unread_messages_count2]
)

I changed the ash_sql dependency to point to a local clone of ash_sql (latest main).

I don’t know if it helps, but here’s the context value that goes along with that ref in AshSql.Expr:

[(ash_sql 0.2.59) lib/expr.ex:1915: AshSql.Expr.default_dynamic_expr/6]
bindings #=> %{
  sort: [],
  domain: GF.Domain,
  context: %{
    private: %{
      actor: nil,
      authorize?: false,
      tenant: nil,
      in_before_action?: true
    },
    action: %Ash.Resource.Actions.Read{
      arguments: [],
      description: nil,
      filter: nil,
      filters: [],
      get_by: [],
      get?: false,
      manual: nil,
      metadata: [],
      skip_unknown_inputs: [],
      modify_query: nil,
      multitenancy: :enforce,
      name: :read,
      pagination: %Ash.Resource.Actions.Read.Pagination{
        default_limit: nil,
        max_page_size: 250,
        countable: true,
        required?: false,
        keyset?: true,
        offset?: true
      },
      preparations: [],
      primary?: true,
      touches_resources: [],
      timeout: nil,
      transaction?: false,
      type: :read
    }
  },
  bindings: %{0 => %{type: :root, path: [], source: GF.Messaging.Participant2}},
  resource: GF.Messaging.Participant2,
  sql_behaviour: AshPostgres.SqlImplementation,
  calculations: %{},
  parent_resources: [],
  aggregate_names: %{},
  parent?: true,
  in_group?: false,
  refs_at_path: [],
  expression_accumulator: %AshSql.Expr.ExprInfo{has_error?: false},
  current: 1,
  root_binding: 0,
  calculation_names: %{},
  current_calculation_name: :calculation_0,
  aggregate_defs: %{
    unread_messages_count2: #count<
      conversation.messages from #Ash.Query<resource: GF.Messaging.Message2,
       filter: #Ash.Filter<inserted_at == parent(last_read_message.inserted_at)>>
    >
  },
  current_aggregate_name: :aggregate_0,
  lateral_join?: false
}

GF.Messaging.Participant2 is the name my application uses for ConversationUser.

Hmm…and what is ref at that point? can you inspect(ref, structs: false)?

Something definitely seems off there. Does adding authorize?: false work with the singly nested parent/1?

ref was last_read_message.inserted_at, which is part of the error message in the output:

raise "Error while building reference: #{inspect(ref)}"

I’m not sure what data type that is. It doesn’t inspect like a string or an atom. I tried structs: false, and it looked the same.

That last error is with the single parent and authorize?: false:

filter expr(inserted_at > parent(last_read_message.inserted_at))

It should be a %Ref{} struct, structs: false should definitely show it differently.

Ah, I made a separate IO.inspect, and this time the struct is exposed:

%{
  attribute: %{
    default: &DateTime.utc_now/0,
    name: :inserted_at,
    type: Ash.Type.UtcDatetimeUsec,
    description: nil,
    source: :inserted_at,
    __struct__: Ash.Resource.Attribute,
    constraints: [
      precision: :microsecond,
      cast_dates_as: :start_of_day,
      timezone: :utc
    ],
    public?: true,
    update_default: nil,
    filterable?: true,
    sortable?: true,
    sensitive?: false,
    allow_nil?: false,
    generated?: false,
    primary_key?: false,
    writable?: false,
    always_select?: false,
    select_by_default?: true,
    match_other_defaults?: true
  },
  resource: GF.Messaging.Message2,
  __struct__: Ash.Query.Ref,
  relationship_path: [:last_read_message],
  simple_equality?: nil,
  input?: false,
  bare?: nil
}

I’ve got an idea.

2 Likes

Fixed in v0.2.60 of ash_sql, regression test added in ash_postgres (ash_sql is always tested “through” one of the data layers, not on its own). Thank you for your patience :smile:

I would definitely like to improve the errors around parent expressions, it just happens to be one of the most complicated part of expr/1 :sweat_smile:

1 Like

By “improve” I mean “implement anything at all better than uninterpretable explosions”

Fixed in v0.2.60 of ash_sql , regression test added in ash_postgres (ash_sql is always tested “through” one of the data layers, not on its own). Thank you for your patience :smile:

Wow, cool! The error is gone!

Now my test is still failing, because the returned aggregate is wrong, so I’m troubleshooting that…

Ok, fixed. I somehow added a bug to the filter expression. Now fixed, and it’s all working.

Here’s the final aggregate, with the check for a null last_read_message_id:

count :unread_messages_count, [:conversation, :messages] do
  filter expr(
           is_nil(parent(last_read_message_id)) or
             inserted_at > parent(last_read_message.inserted_at)
         )

  public? true
end
2 Likes