moxley

moxley

I want to calculate the number of unread Posts for each User in a list of User IDs. Read (verb, past tense) Posts are tracked with PostView, by recording the ID of the last read Post for a given User ID.

Here’s the SQL of what I need:

select
  u.id user_id,
  case
    when pv.user_id is not null then pv.count
    else p.count
  end unread_posts_count
from users u
left join (
  select pv.user_id, count(p2.id) "count"
  from post_views pv
  left join posts p1 on p1.id = pv.last_post_id
  left join posts p2 on p2.inserted_at > p1.inserted_at and p2.archived_at is null
  group by pv.member_id
) tp on tp.user_id = u.id
left join (
  select count(p.id), t.id tenant_id
  from tenants t
  left join posts p on p.tenant_id = t.id
  group by t.id
) p on p.tenant_id = u.tenant_id
where u.id in $1

It seems Ash wants you to use the aggregate DSL combined with relationships to do this. I modified test/actions/has_many_test.exs from the Ash code repo to approximate what my app is doing. Also, I added an attribute to the respective resources to represent a tenant ID, because my app is multitenant.

  defmodule Post do
  ...
      attribute :tenant_id, :string, public?: true
  ...
  end

  defmodule PostView do
    @moduledoc false
    use Ash.Resource,
      domain: Ash.Test.Actions.HasManyTest.Domain,
      data_layer: Ash.DataLayer.Ets

    ets do
      private?(true)
    end

    actions do
      default_accept :*
      defaults [:read, :destroy, create: :*, update: :*]
    end

    attributes do
      uuid_primary_key :id

      attribute :last_post_id, :uuid, public?: true
      attribute :user_id, :uuid, public?: true
    end

    relationships do
      belongs_to :user, Ash.Test.Actions.HasManyTest.User do
        source_attribute :user_id
        destination_attribute :id
        public?(true)
      end

      belongs_to :last_post, Post do
        source_attribute :last_post_id
        destination_attribute :id
        public?(true)
      end
    end
  end

  defmodule User do
    @moduledoc false
    use Ash.Resource,
      domain: Ash.Test.Actions.HasManyTest.Domain,
      data_layer: Ash.DataLayer.Ets

    ets do
      private?(true)
    end

    actions do
      default_accept :*
      defaults [:read, :destroy, create: :*, update: :*]
    end

    attributes do
      uuid_primary_key :id

      attribute :name, :string, public?: true
      attribute :tenant_id, :string, public?: true
    end

    relationships do
      has_one :post_view, PostView do
        source_attribute :id
        destination_attribute :user_id
        public?(true)
      end

      has_many :unread_posts, Post do
        source_attribute :tenant_id
        destination_attribute :tenant_id
        public?(true)

        # This is eventually what I want to do, but it always produces no matches
        # filter expr(inserted_at > parent(post_view.last_post.inserted_at))

        # After some experimentation, I found that this filters as expected
        filter expr(id != parent(post_view.last_post_id))
        # But this never matches, even though it seems equivalent
        filter expr(id != parent(post_view.last_post.id))
      end
    end
  end

So it seems that when the parent(...) expression reaches more than two layers deep, nothing matches, but if it reaches only two layers, it does. I also tried parent(...).id, but that gave an error.

Showing Posts 1 to 10

zachdaniel

zachdaniel

Creator of Ash

Hm…this does seem like a bug. Can you reproduce it in a repo I can test against? I’ll look tomorrow morning.

zachdaniel

zachdaniel

Creator of Ash

Something to try would be setting authorize? false on the aggregate, as it by default filters records according to authorization policies. Can users view post_views?

moxley

moxley OP

moxley

moxley OP

Well, in the example reproduction, I haven’t added the aggregate definition yet. The expression in the relationship needs to work correctly first.

zachdaniel

zachdaniel

Creator of Ash

Perfect thank you. I will investigate tomorrow morning and get back to you :bowing_man:

zachdaniel

zachdaniel

Creator of Ash

Alright, so, the issue in the test reproduction you provided is a bit insidious and I’m not sure if it relates to the actual issue you are seeing because it seems isolated to the way that the ETS data layer computes aggregates.

the id type of Post is :ci_string in that test.

      uuid_primary_key :id, type: :ci_string

But the attribute type of the relationship was :uuid.

      attribute :last_post_id, :uuid, public?: true

Changing that to :ci_string resolves the issue.

The reason this causes a bug is because we check the :source_attribute’s type to see if it supports simple_equality?/0, which means "can we compare values of this with ==. If we can, we use Elixir’s builtin maps for grouping and connecting related entities. This is actually a critical optimization, all Ash apps would be dramatically slower if we didn’t do this.

Due to this misconfiguration, we tried somewhere to compare a CiString struct with a string, and it was not equivalent.

There is supposed to be a compiler warning about types not matching here, not sure why there wasn’t, that can be investigated separately.

So the question is whether or not this has anything to do with your own issue as well, or if its just a red herring in the tests.

zachdaniel

zachdaniel

Creator of Ash

If this doesn’t relate to your root issue, I’ll need a Postgres-specific reproduction. I would do it myself but my stack is Yuge at the moment.

moxley

moxley OP

Here’s an AshPostgres solution:

The current issues are:

  1. When loading the aggregate that delegates to the previously-mentioned relationship, it raises an error. See the test for details.
  2. I can’t figure out how to add the clause to the filter expression to cover one of the required use cases. The details are in the test file.

Feel free to checkout that repo and run the tests.

zachdaniel

zachdaniel

Creator of Ash

Will look tomorrow :+1:, thank you for the detailed reproduction.

zachdaniel

zachdaniel

Creator of Ash

Did some basic investigation, and it’s just a “good old fashioned bug”. The issue is pretty much what you described at the outset, and it stems from essentially a missing set of logic to add the required joins for parent expressions that are contained in relationships used by aggregate queries to the parent query itself.

I’ve pushed a fix to ash_sql main which you can try. It’s not complete but should cover your current case. There is still an issue specifically around if that parent expr itself contains another aggregate that will take me a bit longer to nail down.

Tomorrow or early next week I will transplant your tests into ash_postgres as regression tests and fix the other bug that I mentioned around aggregates.

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews