From 7546fdb7c73352f4063419b74ea77425bb1d8c2b Mon Sep 17 00:00:00 2001 From: Alan Heywood Date: Wed, 3 Dec 2025 14:29:15 +1000 Subject: [PATCH 1/2] test: aggregate with parent ref in relationship filter and sorting on relationship field --- test/aggregate_test.exs | 76 ++++++++++++++++++++++++++++++++++ test/support/resources/chat.ex | 10 +++++ 2 files changed, 86 insertions(+) diff --git a/test/aggregate_test.exs b/test/aggregate_test.exs index a96d4ee7..51514da3 100644 --- a/test/aggregate_test.exs +++ b/test/aggregate_test.exs @@ -8,6 +8,7 @@ defmodule AshSql.AggregateTest do alias AshPostgres.Test.{Author, Chat, Comment, Organization, Post, Rating, User} require Ash.Query + require Ash.Sort import Ash.Expr test "nested sum aggregates" do @@ -1920,6 +1921,81 @@ defmodule AshSql.AggregateTest do end end + test "aggregate with parent() ref in relationship filter and sorting on relationship field" do + chat_1 = + Chat + |> Ash.Changeset.for_create(:create, %{name: "Test Chat"}) + |> Ash.create!() + + chat_1_message_1 = + AshPostgres.Test.Message + |> Ash.Changeset.for_create(:create, %{ + chat_id: chat_1.id, + content: "First message", + sent_at: DateTime.add(DateTime.utc_now(), -3600, :second) + }) + |> Ash.create!() + + _chat_1_message_2 = + AshPostgres.Test.Message + |> Ash.Changeset.for_create(:create, %{ + chat_id: chat_1.id, + content: "Second message", + sent_at: DateTime.add(DateTime.utc_now(), -1800, :second) + }) + |> Ash.create!() + + # Update chat to set last_read_message to the first message + # This means message_2 should be "unread" + _chat = + chat_1 + |> Ash.Changeset.for_update(:update, %{last_read_message_id: chat_1_message_1.id}) + |> Ash.update!() + + # Create a second chat to force multiple records and trigger DISTINCT ON + chat_2 = + Chat + |> Ash.Changeset.for_create(:create, %{name: "Test Chat 2"}) + |> Ash.create!() + + chat_2_message_1 = + AshPostgres.Test.Message + |> Ash.Changeset.for_create(:create, %{ + chat_id: chat_2.id, + content: "Chat 2 - Message 1", + sent_at: DateTime.add(DateTime.utc_now(), -100, :second) + }) + |> Ash.create!() + + AshPostgres.Test.Message + |> Ash.Changeset.for_create(:create, %{ + chat_id: chat_2.id, + content: "Chat 2 - Message 2", + sent_at: DateTime.utc_now() + }) + |> Ash.create!() + + chat_2 + |> Ash.Changeset.for_update(:update, %{last_read_message_id: chat_2_message_1.id}) + |> Ash.update!() + + # This query should work but fails without the fix: + # - select() excludes last_read_message_id from the query + # - Sorting by last_message.sent_at (has_one from_many?) causes DISTINCT ON + subquery wrapping + # - Loading unread_messages_count_alt (aggregate on relationship with parent() in filter) + # uses a lateral join that references parent(last_read_message_id) + # - The wrapped subquery doesn't include last_read_message_id, so the lateral join fails + result = + Chat + |> Ash.Query.filter(id in [^chat_1.id, ^chat_2.id]) + |> Ash.Query.select([:id, :name]) + |> Ash.Query.load(:unread_messages_count_alt) + |> Ash.Query.sort([{Ash.Sort.expr_sort(expr(last_message.sent_at)), :asc}]) + |> Ash.read!() + + assert length(result) == 2 + end + test "multiple aggregates filtering on nested first aggregate" do post = Post diff --git a/test/support/resources/chat.ex b/test/support/resources/chat.ex index afe2bec1..4a819f19 100644 --- a/test/support/resources/chat.ex +++ b/test/support/resources/chat.ex @@ -46,6 +46,12 @@ defmodule AshPostgres.Test.Chat do filter(expr(is_nil(read_at))) sort(sent_at: :desc) end + + has_many :unread_messages, AshPostgres.Test.Message do + public?(true) + no_attributes?(true) + filter(expr(is_nil(parent(last_read_message_id)) or id > parent(last_read_message_id))) + end end aggregates do @@ -53,5 +59,9 @@ defmodule AshPostgres.Test.Chat do public?(true) filter(expr(is_nil(parent(last_read_message_id)) or id > parent(last_read_message_id))) end + + count :unread_messages_count_alt, :unread_messages do + public?(true) + end end end From 15f2b363ed450652bc1f3efe7f1358b2ff94e48b Mon Sep 17 00:00:00 2001 From: Alan Heywood Date: Wed, 3 Dec 2025 14:46:39 +1000 Subject: [PATCH 2/2] chore: reword test comment --- test/aggregate_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/aggregate_test.exs b/test/aggregate_test.exs index 51514da3..cf8bc46d 100644 --- a/test/aggregate_test.exs +++ b/test/aggregate_test.exs @@ -1979,12 +1979,12 @@ defmodule AshSql.AggregateTest do |> Ash.Changeset.for_update(:update, %{last_read_message_id: chat_2_message_1.id}) |> Ash.update!() - # This query should work but fails without the fix: + # This query exercises the following conditions: # - select() excludes last_read_message_id from the query # - Sorting by last_message.sent_at (has_one from_many?) causes DISTINCT ON + subquery wrapping # - Loading unread_messages_count_alt (aggregate on relationship with parent() in filter) # uses a lateral join that references parent(last_read_message_id) - # - The wrapped subquery doesn't include last_read_message_id, so the lateral join fails + # - The wrapped subquery must include last_read_message_id for the lateral join to work result = Chat |> Ash.Query.filter(id in [^chat_1.id, ^chat_2.id])